@aventara/nest 1.0.0-rc.0 → 1.0.0-rc.1
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 +164 -0
- package/README.md +133 -58
- package/dist/aventara.module.d.ts +19 -8
- package/dist/aventara.module.js +18 -6
- package/dist/express-protocol.mounter.d.ts +13 -3
- 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 +7 -2
- package/dist/internal-failure.logger.d.ts +3 -3
- package/dist/internal-failure.logger.js +4 -2
- package/dist/mcp-binding.loader.d.ts +30 -0
- package/dist/mcp-binding.loader.js +25 -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 +13 -3
- package/dist/protocol-host.factory.js +21 -10
- package/dist/protocol-route.table.d.ts +11 -1
- package/dist/protocol-route.table.js +6 -2
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,169 @@
|
|
|
1
1
|
# @aventara/nest
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e3d06f: Breaking: the configuration has a new shape. Every layer (the root, `application` and `client`) now holds the
|
|
8
|
+
same six keys: `exposed`, `transactionsStyle`, `limits`, `pipelines`, `resources` and `exemptions`. Everything about
|
|
9
|
+
one Resource lives under `resources.<Resource>`: its fields, and each field's `definition` and `pipelines`. The old
|
|
10
|
+
keys are refused when the framework starts, with a message that names where each one moved:
|
|
11
|
+
|
|
12
|
+
| Before (rc.0) | Now (rc.1) |
|
|
13
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
14
|
+
| `fields: { Post: { url: { type, nullable, … } } }` | `resources: { Post: { fields: { url: { definition: { type, nullable, … } } } } }` |
|
|
15
|
+
| `behaviors: { Post: { fields: { slug: { create } } } }` | `resources: { Post: { fields: { slug: { pipelines: { compute: { create } } } } } }` |
|
|
16
|
+
| `transactions: "none"` | `transactionsStyle: "none"` |
|
|
17
|
+
| `restrictions` | `exposed` + `exemptions` (see below) |
|
|
18
|
+
| `@aventara/core/json` | `@aventara/core/avz` |
|
|
19
|
+
|
|
20
|
+
Before:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
createFramework({
|
|
24
|
+
api: "/api",
|
|
25
|
+
adapter,
|
|
26
|
+
transactions: "interactive",
|
|
27
|
+
fields: {
|
|
28
|
+
Post: {
|
|
29
|
+
url: {
|
|
30
|
+
type: { scalar: "string" },
|
|
31
|
+
nullable: false,
|
|
32
|
+
list: false,
|
|
33
|
+
lifecycle: ["VIRTUAL"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
behaviors: {
|
|
38
|
+
Post: {
|
|
39
|
+
fields: {
|
|
40
|
+
url: {
|
|
41
|
+
read: {
|
|
42
|
+
dependsOn: ["slug"],
|
|
43
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
createFramework({
|
|
56
|
+
api: "/api",
|
|
57
|
+
adapter,
|
|
58
|
+
transactionsStyle: "interactive",
|
|
59
|
+
resources: {
|
|
60
|
+
Post: {
|
|
61
|
+
fields: {
|
|
62
|
+
url: {
|
|
63
|
+
definition: {
|
|
64
|
+
type: { scalar: "string" },
|
|
65
|
+
nullable: false,
|
|
66
|
+
list: false,
|
|
67
|
+
lifecycle: ["VIRTUAL"],
|
|
68
|
+
format: "url",
|
|
69
|
+
},
|
|
70
|
+
pipelines: {
|
|
71
|
+
compute: {
|
|
72
|
+
read: {
|
|
73
|
+
dependsOn: ["slug"],
|
|
74
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
What else changed:
|
|
86
|
+
|
|
87
|
+
- A field is defined at the root only. A `definition` in `application` or `client` is refused
|
|
88
|
+
(`CONFIG_FORBIDDEN_IN_OVERLAY`). Define the field
|
|
89
|
+
at the root; `application` and `client` can still add compute stages to it.
|
|
90
|
+
- Computed values chain. When the root and an overlay both compute a field, the root's stage runs first and the
|
|
91
|
+
overlay's receives its result as `value`. A stage can no longer be turned off with `false`. Returning `undefined`
|
|
92
|
+
keeps the current value. An upsert computes its create branch with `compute.create` and its update branch with
|
|
93
|
+
`compute.update`.
|
|
94
|
+
- A discovered field keeps the adapter's facts. On a field the adapter discovered you may restate its own `type`,
|
|
95
|
+
`nullable` and `list`, and give a Json field its shape (`definition.json`). Setting a different type, any `format`
|
|
96
|
+
or any `capabilities` on it is refused. `format` is for fields you add, and it must be one of zod's named string
|
|
97
|
+
formats (`"email"`, `"url"`, `"uuid"`, …); any other name is a type error and refused at startup.
|
|
98
|
+
- A misplaced key is a type error. In a configuration written inline in `createFramework({ ... })` or
|
|
99
|
+
`AventaraModule.forRoot`, a key that has no place there (a typo, an old key, `compute` on a relation field, a field
|
|
100
|
+
name neither your schema nor the root has) is now a type error at that key, and still refused at startup.
|
|
101
|
+
- A Resource whose name starts with `$` is refused at startup (`$` names are the framework's).
|
|
102
|
+
- The Json shape builder moved from `@aventara/core/json` to `@aventara/core/avz`. Change the import; `AvZ` is
|
|
103
|
+
the same.
|
|
104
|
+
|
|
105
|
+
`exposed` and `exemptions`, which replace `restrictions`, have their own entry.
|
|
106
|
+
|
|
107
|
+
- 4907afc: New: `AventaraModule.forRoot` and `forRootAsync` take a `state` option that seeds each protocol request's
|
|
108
|
+
`ctx.$state` (see `@aventara/core`'s changelog). It is called once per request with the platform's own request, so
|
|
109
|
+
what your middleware set there reaches every guard, pipe, hook and interceptor of the request:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
AventaraModule.forRoot({
|
|
113
|
+
config,
|
|
114
|
+
state: (req: Request) => ({ user: req.user }), // Passport's req.user, on Express
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
On Express the request is Express's `req`. On Fastify it is Fastify's `request`; middleware added with `app.use`
|
|
119
|
+
writes to its Node request, `request.raw`, so read `request.raw.user` there. Fastify answers a protocol route in its
|
|
120
|
+
`onRequest` hook, so only what runs before that (`app.use` middleware, an earlier `onRequest` hook) can be read. A
|
|
121
|
+
`state` function that throws an `AvError` answers that error (`AvError("A4001", …)` is a `403`); any other throw is
|
|
122
|
+
`500 A3000`. A request the protocol refuses before running (a stale client, an unknown route) never calls it. A
|
|
123
|
+
`state` that is not a function is refused while Nest builds the application.
|
|
124
|
+
|
|
125
|
+
- Breaking: the configuration's mount path is `api` (it was `entrypoint`), and the module reads `framework.api`.
|
|
126
|
+
`forRoot({ config: { api: "/api", adapter } })`. The Fastify refusal of a path with `*` now says "Change `api`, or
|
|
127
|
+
host on Express."
|
|
128
|
+
|
|
129
|
+
- New: with `mcp` in the configuration, the module mounts the MCP endpoint of `@aventara/mcp` at that path (and, with
|
|
130
|
+
`mcp.authorization`, its OAuth metadata at `/.well-known/oauth-protected-resource` plus the path), on Express and on
|
|
131
|
+
Fastify, as it mounts the protocol's routes: the body read raw, the request's URL and peer address handed over, the
|
|
132
|
+
answer written as it is. The `state` option seeds each tool call's `ctx.$state`. `@aventara/mcp` is an optional peer
|
|
133
|
+
dependency, loaded only when `mcp` is configured; with `mcp` set and the package missing, the application refuses to
|
|
134
|
+
start and names it.
|
|
135
|
+
|
|
136
|
+
- Fixed on Express: a request whose path matched a mounted route only because Express ignores case reached the route
|
|
137
|
+
without its body, and was answered as if it had sent none. Express matches routes case-insensitively by default,
|
|
138
|
+
while the body reader compared the path exactly. The MCP routes now answer their own path only, exactly as written,
|
|
139
|
+
so `/MCP` or `/mcp/` goes on to Nest's `404`, as on Fastify. The protocol's routes keep Express's matching (it also
|
|
140
|
+
answers the protocol under `/API` for `api: "/api"`), and the body reader now matches the `api` path the same way, so
|
|
141
|
+
`POST /API/_resources/…` gets its body.
|
|
142
|
+
|
|
143
|
+
- The host's log line for an internal failure names the MCP endpoint when the endpoint itself failed
|
|
144
|
+
(`A3000 on the MCP endpoint (agent origin, request …)`).
|
|
145
|
+
|
|
146
|
+
- Updated dependencies [800123b]
|
|
147
|
+
- Updated dependencies [8bb1d6a]
|
|
148
|
+
- Updated dependencies [68d4395]
|
|
149
|
+
- Updated dependencies [7e3d06f]
|
|
150
|
+
- Updated dependencies [1b5d3c0]
|
|
151
|
+
- Updated dependencies [6317811]
|
|
152
|
+
- Updated dependencies [6d0e5d6]
|
|
153
|
+
- Updated dependencies [7a0cab9]
|
|
154
|
+
- Updated dependencies [eb2a718]
|
|
155
|
+
- Updated dependencies [03312c5]
|
|
156
|
+
- Updated dependencies [123f6dc]
|
|
157
|
+
- Updated dependencies [b65df2e]
|
|
158
|
+
- Updated dependencies [c6e5010]
|
|
159
|
+
- Updated dependencies [c1f9389]
|
|
160
|
+
- Updated dependencies [9c52482]
|
|
161
|
+
- Updated dependencies [8396148]
|
|
162
|
+
- Updated dependencies [e8fbb02]
|
|
163
|
+
- Updated dependencies [916e5f6]
|
|
164
|
+
- Updated dependencies [6cbd158]
|
|
165
|
+
- @aventara/core@1.0.0-rc.1
|
|
166
|
+
|
|
3
167
|
## 1.0.0-rc.0
|
|
4
168
|
|
|
5
169
|
### 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,97 @@ 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 |
|
|
153
173
|
|
|
154
|
-
|
|
155
|
-
`transport.headers`
|
|
156
|
-
|
|
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).
|
|
157
177
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
});
|
|
195
|
+
|
|
196
|
+
// In the configuration: a guard reads it.
|
|
197
|
+
const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
|
|
198
|
+
```
|
|
199
|
+
|
|
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
|
+
- Throw `new AvError("A4001", "…")` from `state` to refuse the request (`403`); any other throw is `500 A3000`, with the
|
|
207
|
+
error in your `diagnostics` sink.
|
|
208
|
+
- A plan sent to `/_transactions` gets one state for all its steps and transaction stages. A ctx call a stage makes
|
|
209
|
+
inherits it. Compute reads it but cannot write it.
|
|
210
|
+
- Server code that calls the framework in process seeds it per call:
|
|
211
|
+
`framework.application.Post.create.one(args, { state: { user } })`.
|
|
212
|
+
|
|
213
|
+
`AvConfig.api` alone decides where the protocol is mounted. `app.setGlobalPrefix(...)` does not move it; set
|
|
214
|
+
`api` to where you want it, and point the generated client at the same URL.
|
|
160
215
|
|
|
161
216
|
## Requests
|
|
162
217
|
|
|
163
|
-
-
|
|
218
|
+
- Bodies on protocol routes are read raw, never through Nest's body parser, up to `limits.maxRequestBytes`. The
|
|
164
219
|
protocol's decoder answers an oversized body (`413 A2010`), another content type (`415 A2011`) and malformed JSON
|
|
165
220
|
(`400 A2000`). Your own routes keep Nest's parsed `req.body`. No `NestFactory.create` option is needed.
|
|
166
|
-
-
|
|
221
|
+
- For `Content-Encoding`, `gzip`, `deflate` and `br` are inflated, and the limit counts decoded bytes. Any other coding
|
|
167
222
|
answers `415 A2011`; a body that does not decode in the coding it names answers `400 A2000`. These two answers carry a
|
|
168
223
|
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
|
-
|
|
224
|
+
- The request id is the client's `Aventara-Request-Id` as it arrived, or one core mints. A middleware that rewrites
|
|
225
|
+
`req.headers` cannot change it, and `X-Request-Id` is never copied into it, so set `Aventara-Request-Id` at your
|
|
226
|
+
proxy.
|
|
227
|
+
- A query string is not part of the protocol path; it is ignored.
|
|
228
|
+
|
|
229
|
+
## The MCP endpoint
|
|
230
|
+
|
|
231
|
+
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 the same
|
|
233
|
+
version:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm install @aventara/mcp@rc
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
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. As for the protocol's routes, the module only mounts: the endpoint's body is read
|
|
241
|
+
raw, the request's URL comes from the platform's `protocol` and `host` (set `trust proxy` on Express or `trustProxy` on
|
|
242
|
+
Fastify behind a proxy), and every answer is `@aventara/mcp`'s. The `state` option seeds each tool call's `ctx.$state`.
|
|
243
|
+
See `@aventara/mcp`'s README for the tools, the contract resource and authorization.
|
|
172
244
|
|
|
173
245
|
## Platform differences
|
|
174
246
|
|
|
175
|
-
-
|
|
176
|
-
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an
|
|
247
|
+
- Fastify cannot mount some Resource keys or `api` paths as literal routes. A Resource key that needs `*` or a
|
|
248
|
+
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an `api` path containing `*`, is refused at startup
|
|
177
249
|
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
|
-
|
|
250
|
+
- Express routing is case-insensitive by default, so it also answers the protocol under, say, `/API/_contract`;
|
|
251
|
+
Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change. The
|
|
252
|
+
MCP endpoint is the exception: on both platforms it answers its own path only, exactly as written (`/mcp`, not
|
|
253
|
+
`/MCP` or `/mcp/`).
|
|
254
|
+
- Express answers `500 A3000` to a protocol route requested with a percent-encoding a generated client never sends
|
|
255
|
+
(for example `%6Eotes` for `notes`); Fastify and the in-process protocol execute it. This is a known divergence.
|
|
182
256
|
- On Express the response carries Express's `X-Powered-By` header; `app.disable("x-powered-by")` removes it.
|
|
183
257
|
|
|
184
258
|
## License
|
|
185
259
|
|
|
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
|
|
260
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
261
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
262
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AvAdapterModel, type AvConfig, type AvConfigAny, type AvConfigExact, type AvFramework, type AvState } from "@aventara/core";
|
|
2
2
|
import { type DynamicModule, type FactoryProvider, type ModuleMetadata, type NestModule, type OnModuleInit } from "@nestjs/common";
|
|
3
3
|
import { type PendingProtocolHost } from "./protocol-host.factory.js";
|
|
4
4
|
/**
|
|
5
5
|
* How the application's Framework enters Nest: your own instance
|
|
6
6
|
* (`{ framework }`, from `createFramework`) or a configuration the module
|
|
7
|
-
* passes to `createFramework` itself (`{ config }`). Pass exactly one.
|
|
7
|
+
* passes to `createFramework` itself (`{ config }`). Pass exactly one. Either
|
|
8
|
+
* arm may add `state`, which seeds each protocol request's `ctx.$state`.
|
|
8
9
|
*/
|
|
9
|
-
export type AventaraModuleOptions<M extends
|
|
10
|
-
readonly framework:
|
|
10
|
+
export type AventaraModuleOptions<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAny> = ({
|
|
11
|
+
readonly framework: AvFramework;
|
|
11
12
|
readonly config?: never;
|
|
12
13
|
} | {
|
|
13
|
-
readonly config: Cfg &
|
|
14
|
+
readonly config: Cfg & AvConfig<M, Cfg> & AvConfigExact<M, Cfg>;
|
|
14
15
|
readonly framework?: never;
|
|
16
|
+
}) & {
|
|
17
|
+
/**
|
|
18
|
+
* Seeds a protocol request's `ctx.$state`, once per request, with the
|
|
19
|
+
* platform's own request: Express's `req`, or Fastify's `request` (its Node
|
|
20
|
+
* request is `request.raw`). What middleware already set there, such as
|
|
21
|
+
* Passport's `req.user`, flows into every stage of the request. A throw is
|
|
22
|
+
* answered like any developer failure: a thrown `AvError` as its own
|
|
23
|
+
* outcome, anything else `500 A3000`. Omitted, the state is `{}`.
|
|
24
|
+
*/
|
|
25
|
+
state?(request: unknown): AvState | Promise<AvState>;
|
|
15
26
|
};
|
|
16
27
|
/**
|
|
17
28
|
* `forRootAsync`'s options: the Nest idiom when what builds the Framework
|
|
18
29
|
* (a Prisma client, a configuration service) is itself a provider. The
|
|
19
30
|
* factory resolves to either arm of {@link AventaraModuleOptions}.
|
|
20
31
|
*/
|
|
21
|
-
export type AventaraModuleAsyncOptions<M extends
|
|
32
|
+
export type AventaraModuleAsyncOptions<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAny> = {
|
|
22
33
|
/** Modules whose providers the factory injects. */
|
|
23
34
|
readonly imports?: ModuleMetadata["imports"];
|
|
24
35
|
/** The providers passed to `useFactory`, in order. */
|
|
@@ -43,11 +54,11 @@ export declare class AventaraModule implements NestModule, OnModuleInit {
|
|
|
43
54
|
*/
|
|
44
55
|
onModuleInit(): void;
|
|
45
56
|
/** Registers the module with a Framework instance or a configuration. */
|
|
46
|
-
static forRoot<M extends
|
|
57
|
+
static forRoot<M extends AvAdapterModel, const Cfg = AvConfigAny>(options: AventaraModuleOptions<M, Cfg>): DynamicModule;
|
|
47
58
|
/**
|
|
48
59
|
* Registers the module with options built by a factory, for when the
|
|
49
60
|
* Framework's inputs (a Prisma client, a config service) are providers.
|
|
50
61
|
*/
|
|
51
|
-
static forRootAsync<M extends
|
|
62
|
+
static forRootAsync<M extends AvAdapterModel, const Cfg = AvConfigAny>(options: AventaraModuleAsyncOptions<M, Cfg>): DynamicModule;
|
|
52
63
|
private static mount;
|
|
53
64
|
}
|
package/dist/aventara.module.js
CHANGED
|
@@ -4,12 +4,14 @@ import { HttpAdapterHost } from "@nestjs/core";
|
|
|
4
4
|
import { AVENTARA_FRAMEWORK } from "./framework.token.js";
|
|
5
5
|
import { checkedOptions, recordMount } from "./host-bootstrap.validator.js";
|
|
6
6
|
import { withInternalFailureLog } from "./internal-failure.logger.js";
|
|
7
|
+
import { loadMcpBinding } from "./mcp-binding.loader.js";
|
|
7
8
|
import { createPendingProtocolHost, } from "./protocol-host.factory.js";
|
|
8
9
|
const MOUNT = Symbol("AVENTARA_MOUNT");
|
|
9
10
|
let mounts = 0;
|
|
10
11
|
const PROTOCOL_HOST = Symbol("AVENTARA_PROTOCOL_HOST");
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const OPTIONS = Symbol("AVENTARA_OPTIONS");
|
|
13
|
+
const MCP_BINDING = Symbol("AVENTARA_MCP_BINDING");
|
|
14
|
+
async function frameworkOf(checked) {
|
|
13
15
|
if (checked.framework !== undefined) {
|
|
14
16
|
return checked.framework;
|
|
15
17
|
}
|
|
@@ -44,17 +46,27 @@ export class AventaraModule {
|
|
|
44
46
|
providers: [
|
|
45
47
|
{ provide: MOUNT, useValue: mounts },
|
|
46
48
|
{
|
|
47
|
-
provide:
|
|
49
|
+
provide: OPTIONS,
|
|
48
50
|
inject: [HttpAdapterHost, ...inject],
|
|
49
51
|
useFactory: async (application, ...deps) => {
|
|
50
52
|
recordMount(application);
|
|
51
|
-
return
|
|
53
|
+
return checkedOptions(await resolve(...deps));
|
|
52
54
|
},
|
|
53
55
|
},
|
|
56
|
+
{
|
|
57
|
+
provide: AVENTARA_FRAMEWORK,
|
|
58
|
+
inject: [OPTIONS],
|
|
59
|
+
useFactory: (options) => frameworkOf(options),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
provide: MCP_BINDING,
|
|
63
|
+
inject: [AVENTARA_FRAMEWORK],
|
|
64
|
+
useFactory: async (framework) => (await loadMcpBinding(framework)) ?? null,
|
|
65
|
+
},
|
|
54
66
|
{
|
|
55
67
|
provide: PROTOCOL_HOST,
|
|
56
|
-
inject: [HttpAdapterHost, AVENTARA_FRAMEWORK],
|
|
57
|
-
useFactory: (application, framework) => createPendingProtocolHost(application, framework),
|
|
68
|
+
inject: [HttpAdapterHost, AVENTARA_FRAMEWORK, OPTIONS, MCP_BINDING],
|
|
69
|
+
useFactory: (application, framework, options, mcp) => createPendingProtocolHost(application, framework, options.state?.bind(options), mcp ?? undefined),
|
|
58
70
|
},
|
|
59
71
|
],
|
|
60
72
|
exports: [AVENTARA_FRAMEWORK],
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import type { AvProtocolBinding } from "@aventara/core/protocol";
|
|
2
|
+
import type { McpBinding } from "./mcp-binding.loader.js";
|
|
2
3
|
import { type NodeIncoming, type NodeOutgoing } from "./node-exchange.translator.js";
|
|
3
|
-
import type { ProtocolHost } from "./protocol-host.factory.js";
|
|
4
|
+
import type { HostState, ProtocolHost } from "./protocol-host.factory.js";
|
|
4
5
|
/**
|
|
5
6
|
* The protocol on Nest's Express platform: one platform route per protocol
|
|
6
7
|
* route under the entrypoint, translating the Express request into the
|
|
7
8
|
* protocol's request and the protocol's response back onto the wire. A path the
|
|
8
9
|
* protocol does not own goes on to Nest.
|
|
9
10
|
*/
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Express's request: Node's `IncomingMessage`, plus `originalUrl`, and the
|
|
13
|
+
* protocol and host Express reports (its `trust proxy` setting applied).
|
|
14
|
+
*/
|
|
11
15
|
type ExpressRequest = NodeIncoming & {
|
|
12
16
|
readonly method: string;
|
|
13
17
|
readonly originalUrl: string;
|
|
18
|
+
readonly protocol: string;
|
|
19
|
+
readonly host: string;
|
|
20
|
+
readonly socket: {
|
|
21
|
+
readonly remoteAddress?: string | undefined;
|
|
22
|
+
};
|
|
14
23
|
};
|
|
15
24
|
/** Express's response, written through Node's own `writeHead`. */
|
|
16
25
|
type ExpressResponse = NodeOutgoing;
|
|
@@ -19,11 +28,12 @@ type ExpressHandler = (request: ExpressRequest, response: ExpressResponse, next?
|
|
|
19
28
|
export type ExpressPlatform = {
|
|
20
29
|
get(path: string, handler: ExpressHandler): unknown;
|
|
21
30
|
post(path: string, handler: ExpressHandler): unknown;
|
|
31
|
+
delete(path: string, handler: ExpressHandler): unknown;
|
|
22
32
|
use(handler: ExpressHandler): unknown;
|
|
23
33
|
};
|
|
24
34
|
/**
|
|
25
35
|
* Installs the pre-parser on `platform` now, and returns the host whose
|
|
26
36
|
* `mount` registers the routes and the absence fallback under `entrypoint`.
|
|
27
37
|
*/
|
|
28
|
-
export declare function createExpressProtocolHost(platform: ExpressPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number): ProtocolHost;
|
|
38
|
+
export declare function createExpressProtocolHost(platform: ExpressPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number, state: HostState | undefined, mcp: McpBinding | undefined): ProtocolHost;
|
|
29
39
|
export {};
|
|
@@ -1,25 +1,42 @@
|
|
|
1
|
-
import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
|
-
import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
|
|
1
|
+
import { mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
|
+
import { MCP_ROUTES, PROTOCOL_ROUTES, } from "./protocol-route.table.js";
|
|
3
3
|
import { readRequestBody, unreadBodyAnswer, } from "./request-body.reader.js";
|
|
4
|
-
const REGISTER = { GET: "get", POST: "post" };
|
|
4
|
+
const REGISTER = { GET: "get", POST: "post", DELETE: "delete" };
|
|
5
5
|
const NOTHING_READ = { kind: "read", content: NO_BYTES };
|
|
6
|
-
export function createExpressProtocolHost(platform, binding, entrypoint, maxRequestBytes) {
|
|
6
|
+
export function createExpressProtocolHost(platform, binding, entrypoint, maxRequestBytes, state, mcp) {
|
|
7
7
|
const bodies = new WeakMap();
|
|
8
8
|
const bodyOf = (request) => bodies.get(request) ?? NOTHING_READ;
|
|
9
|
+
const mcpEndpoint = mcp
|
|
10
|
+
?.surface()
|
|
11
|
+
.find((route) => route.kind === "endpoint")?.path;
|
|
12
|
+
const namespace = asciiLowerCase(`${entrypoint}/_`);
|
|
9
13
|
platform.use(async (request, _response, next) => {
|
|
10
|
-
|
|
14
|
+
const pathname = pathnameOf(request.originalUrl);
|
|
15
|
+
if (asciiLowerCase(pathname.slice(0, namespace.length)) === namespace) {
|
|
11
16
|
bodies.set(request, await readRequestBody(request, request.headersDistinct["content-encoding"], maxRequestBytes));
|
|
12
17
|
}
|
|
18
|
+
else if (mcp !== undefined && pathname === mcpEndpoint) {
|
|
19
|
+
bodies.set(request, await readRequestBody(request, undefined, mcp.maxRequestBytes));
|
|
20
|
+
}
|
|
13
21
|
handOn(next);
|
|
14
22
|
});
|
|
15
23
|
return {
|
|
16
24
|
mount: () => {
|
|
25
|
+
for (const route of mcp?.surface() ?? []) {
|
|
26
|
+
const answer = MCP_ROUTES[route.kind](mcp);
|
|
27
|
+
for (const method of route.methods) {
|
|
28
|
+
platform[REGISTER[method]](literalPath(route.path), exactly(route.path, async (request, response) => {
|
|
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)));
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
for (const route of binding.surface()) {
|
|
18
35
|
const answer = PROTOCOL_ROUTES[route.kind](binding);
|
|
19
36
|
platform[REGISTER[route.method]](literalPath(`${entrypoint}${route.path}`), async (request, response) => {
|
|
20
37
|
const body = bodyOf(request);
|
|
21
38
|
writeAnswer(response, body.kind === "read"
|
|
22
|
-
? await answer(translated(request, entrypoint, body.content))
|
|
39
|
+
? await answer(translated(request, entrypoint, body.content), state === undefined ? undefined : () => state(request))
|
|
23
40
|
: unreadBodyAnswer(binding, body));
|
|
24
41
|
});
|
|
25
42
|
}
|
|
@@ -39,6 +56,18 @@ export function createExpressProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
39
56
|
},
|
|
40
57
|
};
|
|
41
58
|
}
|
|
59
|
+
function exactly(path, handler) {
|
|
60
|
+
return (request, response, next) => {
|
|
61
|
+
if (pathnameOf(request.originalUrl) !== path) {
|
|
62
|
+
handOn(next);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return handler(request, response, next);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function asciiLowerCase(text) {
|
|
69
|
+
return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
70
|
+
}
|
|
42
71
|
function handOn(next) {
|
|
43
72
|
if (typeof next === "function") {
|
|
44
73
|
next();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AvProtocolBinding } from "@aventara/core/protocol";
|
|
2
|
+
import type { McpBinding } from "./mcp-binding.loader.js";
|
|
2
3
|
import { type NodeIncoming, type NodeOutgoing, type PlatformHeaders } from "./node-exchange.translator.js";
|
|
3
|
-
import type { ProtocolHost } from "./protocol-host.factory.js";
|
|
4
|
+
import type { HostState, ProtocolHost } from "./protocol-host.factory.js";
|
|
4
5
|
/**
|
|
5
6
|
* The protocol on Nest's Fastify platform: one Fastify route per protocol route
|
|
6
7
|
* under the entrypoint, answered from its own `onRequest` hook before Fastify's
|
|
@@ -10,13 +11,21 @@ import type { ProtocolHost } from "./protocol-host.factory.js";
|
|
|
10
11
|
* reserved escape) is refused while Nest builds the application, naming the
|
|
11
12
|
* Resource keys.
|
|
12
13
|
*/
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Fastify's request: Node's own under `raw`, whether a route matched, and the
|
|
16
|
+
* protocol and host Fastify reports (its `trustProxy` setting applied).
|
|
17
|
+
*/
|
|
14
18
|
type FastifyRequest = {
|
|
15
19
|
readonly raw: NodeIncoming & {
|
|
16
20
|
readonly method: string;
|
|
17
21
|
readonly url: string;
|
|
22
|
+
readonly socket: {
|
|
23
|
+
readonly remoteAddress?: string | undefined;
|
|
24
|
+
};
|
|
18
25
|
};
|
|
19
26
|
readonly is404: boolean;
|
|
27
|
+
readonly protocol: string;
|
|
28
|
+
readonly host: string;
|
|
20
29
|
};
|
|
21
30
|
/** Fastify's reply: hijacked, then written through Node's response under `raw`. */
|
|
22
31
|
type FastifyReply = {
|
|
@@ -27,7 +36,7 @@ type FastifyReply = {
|
|
|
27
36
|
type OnRequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply | undefined>;
|
|
28
37
|
export type FastifyPlatform = {
|
|
29
38
|
route(options: {
|
|
30
|
-
readonly method: string;
|
|
39
|
+
readonly method: string | readonly string[];
|
|
31
40
|
readonly url: string;
|
|
32
41
|
readonly exposeHeadRoute: false;
|
|
33
42
|
readonly onRequest: OnRequestHook;
|
|
@@ -39,5 +48,5 @@ export type FastifyPlatform = {
|
|
|
39
48
|
* The host whose `mount` registers the routes and the absence hook under
|
|
40
49
|
* `entrypoint` — after refusing, now, a surface it cannot mount literally.
|
|
41
50
|
*/
|
|
42
|
-
export declare function createFastifyProtocolHost(platform: FastifyPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number): ProtocolHost;
|
|
51
|
+
export declare function createFastifyProtocolHost(platform: FastifyPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number, state: HostState | undefined, mcp: McpBinding | undefined): ProtocolHost;
|
|
43
52
|
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
|
-
import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
|
|
1
|
+
import { mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
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;
|
|
5
5
|
function checkLiteralRoutes(binding, entrypoint) {
|
|
6
6
|
if (entrypoint.includes("*")) {
|
|
7
|
-
throw new Error(`AventaraModule cannot mount the
|
|
7
|
+
throw new Error(`AventaraModule cannot mount the api path \`${entrypoint}\` as a literal path on Nest's Fastify platform: it may not carry \`*\`. Change \`api\`, or host on Express.`);
|
|
8
8
|
}
|
|
9
9
|
const refused = binding
|
|
10
10
|
.surface()
|
|
@@ -16,11 +16,35 @@ function checkLiteralRoutes(binding, entrypoint) {
|
|
|
16
16
|
throw new Error(`AventaraModule cannot mount Resource ${refused.map((key) => `\`${key}\``).join(", ")} as literal routes on Nest's Fastify platform: a route path may not carry \`*\` or a reserved character (: + # $ & , / ; = ? @). Rename the Resource, or host on Express.`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
function checkLiteralMcpRoutes(mcp) {
|
|
20
|
+
for (const route of mcp?.surface() ?? []) {
|
|
21
|
+
if (UNMATCHABLE.test(route.path)) {
|
|
22
|
+
throw new Error(`AventaraModule cannot mount the mcp path \`${route.path}\` as a literal path on Nest's Fastify platform: it may not carry \`*\` or a reserved escape. Change \`mcp\`, or host on Express.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequestBytes, state, mcp) {
|
|
20
27
|
checkLiteralRoutes(binding, entrypoint);
|
|
28
|
+
checkLiteralMcpRoutes(mcp);
|
|
21
29
|
const translated = (request, content) => protocolRequest(request.raw.method, request.raw.url, request.raw, entrypoint, content);
|
|
22
30
|
return {
|
|
23
31
|
mount: () => {
|
|
32
|
+
for (const route of mcp?.surface() ?? []) {
|
|
33
|
+
const answer = MCP_ROUTES[route.kind](mcp);
|
|
34
|
+
platform.route({
|
|
35
|
+
method: [...route.methods],
|
|
36
|
+
url: literalPath(route.path),
|
|
37
|
+
exposeHeadRoute: false,
|
|
38
|
+
onRequest: async (request, reply) => {
|
|
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)));
|
|
41
|
+
return reply;
|
|
42
|
+
},
|
|
43
|
+
handler: () => {
|
|
44
|
+
throw new Error("AventaraModule: an MCP route's onRequest hook answers every request; its handler is unreachable.");
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
24
48
|
for (const route of binding.surface()) {
|
|
25
49
|
const answer = PROTOCOL_ROUTES[route.kind](binding);
|
|
26
50
|
platform.route({
|
|
@@ -30,7 +54,7 @@ export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
30
54
|
onRequest: async (request, reply) => {
|
|
31
55
|
const body = await readRequestBody(request.raw, request.raw.headersDistinct["content-encoding"], maxRequestBytes);
|
|
32
56
|
write(reply, body.kind === "read"
|
|
33
|
-
? await answer(translated(request, body.content))
|
|
57
|
+
? await answer(translated(request, body.content), state === undefined ? undefined : () => state(request))
|
|
34
58
|
: unreadBodyAnswer(binding, body));
|
|
35
59
|
return reply;
|
|
36
60
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The injection token under which `AventaraModule` provides the application's
|
|
3
|
-
* `
|
|
3
|
+
* `AvFramework`. Type the injected value with your own `AvFramework<M, Cfg>` alias.
|
|
4
4
|
*/
|
|
5
5
|
export declare const AVENTARA_FRAMEWORK: unique symbol;
|
|
6
6
|
/** `@InjectFramework()`: `@Inject(AVENTARA_FRAMEWORK)` in one call. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const NEITHER_ARM = "AventaraModule: the options carry neither `framework` nor `config`; pass exactly one.";
|
|
2
2
|
const BOTH_ARMS = "AventaraModule: the options carry both `framework` and `config`; pass exactly one.";
|
|
3
|
-
const NOT_A_FRAMEWORK = "AventaraModule: `framework` is not an Aventara Framework (it has no string `
|
|
3
|
+
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
|
+
const STATE_NOT_A_FUNCTION = "AventaraModule: `state` is not a function; pass `(request) => ({ … })`, called once per protocol request.";
|
|
4
5
|
const MOUNTED_TWICE = "AventaraModule is mounted twice in one application; import AventaraModule.forRoot or forRootAsync once.";
|
|
5
6
|
export function checkedOptions(options) {
|
|
6
7
|
const record = typeof options === "object" && options !== null
|
|
@@ -17,6 +18,10 @@ export function checkedOptions(options) {
|
|
|
17
18
|
if (hasFramework && !isFramework(record.framework)) {
|
|
18
19
|
throw new Error(NOT_A_FRAMEWORK);
|
|
19
20
|
}
|
|
21
|
+
const state = record.state;
|
|
22
|
+
if (state !== undefined && typeof state !== "function") {
|
|
23
|
+
throw new Error(STATE_NOT_A_FUNCTION);
|
|
24
|
+
}
|
|
20
25
|
return record;
|
|
21
26
|
}
|
|
22
27
|
function isFramework(value) {
|
|
@@ -24,7 +29,7 @@ function isFramework(value) {
|
|
|
24
29
|
return false;
|
|
25
30
|
}
|
|
26
31
|
const candidate = value;
|
|
27
|
-
return (typeof candidate.
|
|
32
|
+
return (typeof candidate.api === "string" &&
|
|
28
33
|
typeof candidate.contracts?.client === "object" &&
|
|
29
34
|
candidate.contracts.client !== null);
|
|
30
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvDiagnostic, AvDiagnosticsSink } from "@aventara/core";
|
|
2
2
|
/** Wraps the application's sink (if any): log an internal failure, then forward. */
|
|
3
|
-
export declare function withInternalFailureLog(sink:
|
|
3
|
+
export declare function withInternalFailureLog(sink: AvDiagnosticsSink | undefined): AvDiagnosticsSink;
|
|
4
4
|
/**
|
|
5
5
|
* e.g. `A3001 on Post.create.one (client origin, request 7f3c…): Error. The raw
|
|
6
6
|
* error goes to the framework's diagnostics sink.`
|
|
7
7
|
*/
|
|
8
|
-
export declare function internalFailureLine(diagnostic:
|
|
8
|
+
export declare function internalFailureLine(diagnostic: AvDiagnostic): string;
|
|
@@ -15,7 +15,9 @@ export function withInternalFailureLog(sink) {
|
|
|
15
15
|
export function internalFailureLine(diagnostic) {
|
|
16
16
|
const subject = diagnostic.kind === "operation"
|
|
17
17
|
? `${diagnostic.resource}.${diagnostic.family}.${diagnostic.variant}`
|
|
18
|
-
: "
|
|
18
|
+
: diagnostic.kind === "transaction"
|
|
19
|
+
? "a transaction"
|
|
20
|
+
: "the MCP endpoint";
|
|
19
21
|
const step = diagnostic.operation === undefined
|
|
20
22
|
? ""
|
|
21
23
|
: `, plan operation ${diagnostic.operation}`;
|
|
@@ -24,7 +26,7 @@ export function internalFailureLine(diagnostic) {
|
|
|
24
26
|
function frameworkDetail(error) {
|
|
25
27
|
try {
|
|
26
28
|
if (error instanceof Error &&
|
|
27
|
-
error.name === "
|
|
29
|
+
error.name === "AvPipeOutputError" &&
|
|
28
30
|
typeof error.message === "string") {
|
|
29
31
|
return ` — ${error.message}`;
|
|
30
32
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AvFramework, AvState } from "@aventara/core";
|
|
2
|
+
import type { AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
|
+
/** One request to the MCP endpoint, as `@aventara/mcp` reads it. */
|
|
4
|
+
export type McpRequest = {
|
|
5
|
+
readonly method: string;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly headers: Readonly<Record<string, readonly string[]>>;
|
|
8
|
+
readonly body: Uint8Array;
|
|
9
|
+
readonly remoteAddress: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
/** One route the MCP binding lists. */
|
|
12
|
+
export type McpRoute = {
|
|
13
|
+
readonly kind: "endpoint" | "metadata";
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly methods: readonly ("GET" | "POST" | "DELETE")[];
|
|
16
|
+
};
|
|
17
|
+
export type McpBinding = {
|
|
18
|
+
readonly surface: () => readonly McpRoute[];
|
|
19
|
+
readonly maxRequestBytes: number;
|
|
20
|
+
readonly handleEndpoint: (request: McpRequest, options?: {
|
|
21
|
+
readonly state?: () => AvState | Promise<AvState>;
|
|
22
|
+
}) => Promise<AvProtocolResponse>;
|
|
23
|
+
readonly encodeMetadata: (request: McpRequest) => AvProtocolResponse;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The MCP binding for `framework`, or `undefined` when its configuration has no
|
|
27
|
+
* `mcp`. Called while Nest instantiates providers, so a missing package rejects
|
|
28
|
+
* `NestFactory.create` before the application listens, naming the package.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadMcpBinding(framework: AvFramework, specifier?: string): Promise<McpBinding | undefined>;
|
|
@@ -0,0 +1,25 @@
|
|
|
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 beside @aventara/nest, at the same version (npm install ${specifier}), or remove \`mcp\` from the configuration.`;
|
|
3
|
+
function isMissing(error, specifier) {
|
|
4
|
+
const { code, message } = (error ?? {});
|
|
5
|
+
return ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") &&
|
|
6
|
+
typeof message === "string" &&
|
|
7
|
+
message.includes(`'${specifier}'`));
|
|
8
|
+
}
|
|
9
|
+
export async function loadMcpBinding(framework, specifier = MCP_PACKAGE) {
|
|
10
|
+
if (framework.mcp === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
let loaded;
|
|
14
|
+
try {
|
|
15
|
+
loaded = await import(specifier);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isMissing(error, specifier)) {
|
|
19
|
+
throw new Error(missing(specifier));
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const { AvMcp } = loaded;
|
|
24
|
+
return AvMcp.bind(framework);
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
2
|
+
import type { McpRequest } from "./mcp-binding.loader.js";
|
|
2
3
|
export type NodeIncoming = AsyncIterable<Uint8Array> & {
|
|
3
4
|
readonly headersDistinct: Readonly<Record<string, readonly string[]>>;
|
|
4
5
|
};
|
|
@@ -14,6 +15,11 @@ export declare const NO_BYTES: Uint8Array<ArrayBuffer>;
|
|
|
14
15
|
export declare function pathnameOf(url: string): string;
|
|
15
16
|
/** The request as the protocol reads it: path relative to the entrypoint, distinct headers, raw body. */
|
|
16
17
|
export declare function protocolRequest(method: string, url: string, incoming: NodeIncoming, entrypoint: string, content: Uint8Array): AvProtocolRequest;
|
|
18
|
+
/** The request as `@aventara/mcp` reads it: absolute URL, distinct headers, raw body, peer address. */
|
|
19
|
+
export declare function mcpRequest(method: string, origin: {
|
|
20
|
+
readonly protocol: string;
|
|
21
|
+
readonly host: string;
|
|
22
|
+
}, url: string, incoming: NodeIncoming, content: Uint8Array, remoteAddress: string | undefined): McpRequest;
|
|
17
23
|
/**
|
|
18
24
|
* Writes the protocol's answer as it is. Headers the platform's hooks already
|
|
19
25
|
* set (CORS, say) go beneath it; a protocol header of the same name, in any
|
|
@@ -11,6 +11,15 @@ export function protocolRequest(method, url, incoming, entrypoint, content) {
|
|
|
11
11
|
body: { kind: "raw", content },
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
export function mcpRequest(method, origin, url, incoming, content, remoteAddress) {
|
|
15
|
+
return {
|
|
16
|
+
method,
|
|
17
|
+
url: `${origin.protocol}://${origin.host}${url}`,
|
|
18
|
+
headers: incoming.headersDistinct,
|
|
19
|
+
body: content,
|
|
20
|
+
remoteAddress,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
14
23
|
export function writeAnswer(response, answer, beneath = {}) {
|
|
15
24
|
const own = new Set(Object.keys(answer.headers).map((name) => name.toLowerCase()));
|
|
16
25
|
const headers = {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvFramework, AvState } from "@aventara/core";
|
|
2
2
|
import type { AbstractHttpAdapter } from "@nestjs/core";
|
|
3
|
+
import type { McpBinding, McpRoute } from "./mcp-binding.loader.js";
|
|
3
4
|
import type { SurfaceRoute } from "./protocol-route.table.js";
|
|
5
|
+
/** The developer's `state` option: seeds a request's `ctx.$state` from its platform request. */
|
|
6
|
+
export type HostState = (request: unknown) => AvState | Promise<AvState>;
|
|
4
7
|
/** The protocol, bound once at startup, and the one call that mounts it. */
|
|
5
8
|
export type ProtocolHost = {
|
|
6
9
|
readonly mount: () => void;
|
|
@@ -11,7 +14,14 @@ export type ProtocolHost = {
|
|
|
11
14
|
* rejects before the application listens. The routes are mounted later, from
|
|
12
15
|
* the module's `configure()`.
|
|
13
16
|
*/
|
|
14
|
-
export declare function createProtocolHost(adapter: AbstractHttpAdapter | undefined, framework:
|
|
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;
|
|
15
25
|
/**
|
|
16
26
|
* What one mount registered, read off `protocol.surface()`: the operation
|
|
17
27
|
* count under the entrypoint, then each framework route by method and path —
|
|
@@ -35,4 +45,4 @@ export type AdapterHostView = {
|
|
|
35
45
|
* A host for an application whose HTTP adapter may arrive after the providers
|
|
36
46
|
* are instantiated (Nest's testing module).
|
|
37
47
|
*/
|
|
38
|
-
export declare function createPendingProtocolHost(application: AdapterHostView, framework:
|
|
48
|
+
export declare function createPendingProtocolHost(application: AdapterHostView, framework: AvFramework, state?: HostState, mcp?: McpBinding): PendingProtocolHost;
|
|
@@ -4,24 +4,35 @@ import { createExpressProtocolHost } from "./express-protocol.mounter.js";
|
|
|
4
4
|
import { createFastifyProtocolHost, } from "./fastify-protocol.mounter.js";
|
|
5
5
|
import { checkedPlatform, } from "./host-bootstrap.validator.js";
|
|
6
6
|
const PLATFORM_MOUNTERS = {
|
|
7
|
-
express: (adapter, binding, entrypoint, maxRequestBytes) => createExpressProtocolHost({
|
|
7
|
+
express: (adapter, binding, entrypoint, maxRequestBytes, state, mcp) => createExpressProtocolHost({
|
|
8
8
|
get: (path, handler) => adapter.get(path, handler),
|
|
9
9
|
post: (path, handler) => adapter.post(path, handler),
|
|
10
|
+
delete: (path, handler) => adapter.delete(path, handler),
|
|
10
11
|
use: (handler) => adapter.use(handler),
|
|
11
|
-
}, binding, entrypoint, maxRequestBytes),
|
|
12
|
-
fastify: (adapter, binding, entrypoint, maxRequestBytes) => createFastifyProtocolHost(adapter.getInstance(), binding, entrypoint, maxRequestBytes),
|
|
12
|
+
}, binding, entrypoint, maxRequestBytes, state, mcp),
|
|
13
|
+
fastify: (adapter, binding, entrypoint, maxRequestBytes, state, mcp) => createFastifyProtocolHost(adapter.getInstance(), binding, entrypoint, maxRequestBytes, state, mcp),
|
|
13
14
|
};
|
|
14
|
-
export function createProtocolHost(adapter, framework) {
|
|
15
|
+
export function createProtocolHost(adapter, framework, state, mcp) {
|
|
15
16
|
const platform = checkedPlatform(adapter?.getType());
|
|
16
17
|
const binding = AvProtocol.bind(framework);
|
|
17
|
-
const host = PLATFORM_MOUNTERS[platform](adapter, binding, framework.
|
|
18
|
+
const host = PLATFORM_MOUNTERS[platform](adapter, binding, framework.api, framework.contracts.client.limits.maxRequestBytes, state, mcp);
|
|
18
19
|
return {
|
|
19
20
|
mount: () => {
|
|
20
21
|
host.mount();
|
|
21
|
-
MOUNT_LOGGER.log(mountedLine(binding.surface(), framework.
|
|
22
|
+
MOUNT_LOGGER.log(mountedLine(binding.surface(), framework.api));
|
|
23
|
+
if (mcp !== undefined) {
|
|
24
|
+
MOUNT_LOGGER.log(mcpMountedLine(mcp.surface()));
|
|
25
|
+
}
|
|
22
26
|
},
|
|
23
27
|
};
|
|
24
28
|
}
|
|
29
|
+
export function mcpMountedLine(routes) {
|
|
30
|
+
const endpoint = routes.find((route) => route.kind === "endpoint");
|
|
31
|
+
const metadata = routes.find((route) => route.kind === "metadata");
|
|
32
|
+
return `Aventara mounted the MCP endpoint at ${endpoint?.path ?? "/"} (${metadata === undefined
|
|
33
|
+
? "no authorization: it answers callers on this machine only"
|
|
34
|
+
: `+ GET ${metadata.path}`})`;
|
|
35
|
+
}
|
|
25
36
|
const MOUNT_LOGGER = new Logger("AventaraModule");
|
|
26
37
|
export function mountedLine(routes, entrypoint) {
|
|
27
38
|
const operations = routes.filter((route) => route.kind === "operation");
|
|
@@ -30,16 +41,16 @@ export function mountedLine(routes, entrypoint) {
|
|
|
30
41
|
.map((route) => `${route.method} ${entrypoint}${route.path}`);
|
|
31
42
|
return `Aventara mounted ${operations.length} ${operations.length === 1 ? "operation" : "operations"} at ${entrypoint === "" ? "/" : entrypoint} (+ ${framework.join(", ")})`;
|
|
32
43
|
}
|
|
33
|
-
export function createPendingProtocolHost(application, framework) {
|
|
44
|
+
export function createPendingProtocolHost(application, framework, state, mcp) {
|
|
34
45
|
if (application.httpAdapter !== undefined) {
|
|
35
|
-
const host = createProtocolHost(application.httpAdapter, framework);
|
|
46
|
+
const host = createProtocolHost(application.httpAdapter, framework, state, mcp);
|
|
36
47
|
return { mount: host.mount, assertReady: () => undefined };
|
|
37
48
|
}
|
|
38
49
|
let host;
|
|
39
50
|
let refusal;
|
|
40
51
|
application.init$.subscribe(() => {
|
|
41
52
|
try {
|
|
42
|
-
host = createProtocolHost(application.httpAdapter, framework);
|
|
53
|
+
host = createProtocolHost(application.httpAdapter, framework, state, mcp);
|
|
43
54
|
}
|
|
44
55
|
catch (error) {
|
|
45
56
|
refusal = error;
|
|
@@ -49,7 +60,7 @@ export function createPendingProtocolHost(application, framework) {
|
|
|
49
60
|
if (refusal !== undefined) {
|
|
50
61
|
throw refusal;
|
|
51
62
|
}
|
|
52
|
-
return host ?? createProtocolHost(undefined, framework);
|
|
63
|
+
return host ?? createProtocolHost(undefined, framework, state, mcp);
|
|
53
64
|
};
|
|
54
65
|
return {
|
|
55
66
|
mount: () => ready().mount(),
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import type { AvState } from "@aventara/core";
|
|
1
2
|
import type { AvProtocolBinding, AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
3
|
+
import type { McpBinding, McpRequest, McpRoute } from "./mcp-binding.loader.js";
|
|
2
4
|
/** One route `protocol.surface()` lists. */
|
|
3
5
|
export type SurfaceRoute = ReturnType<AvProtocolBinding["surface"]>[number];
|
|
6
|
+
/** The developer's `state` option over one platform request: seeds its `ctx.$state`. */
|
|
7
|
+
export type StateSeed = () => AvState | Promise<AvState>;
|
|
4
8
|
/** What a mounted route answers its request with: a bound protocol member. */
|
|
5
|
-
export type RouteAnswer = (request: AvProtocolRequest) => AvProtocolResponse | Promise<AvProtocolResponse>;
|
|
9
|
+
export type RouteAnswer = (request: AvProtocolRequest, state: StateSeed | undefined) => AvProtocolResponse | Promise<AvProtocolResponse>;
|
|
6
10
|
/** The bound member each kind of surface route is answered by. */
|
|
7
11
|
export type ProtocolRouteTable = {
|
|
8
12
|
readonly [K in SurfaceRoute["kind"]]: (binding: AvProtocolBinding) => RouteAnswer;
|
|
9
13
|
};
|
|
10
14
|
export declare const PROTOCOL_ROUTES: ProtocolRouteTable;
|
|
15
|
+
/** What a mounted MCP route answers its request with: a bound `@aventara/mcp` member. */
|
|
16
|
+
export type McpRouteAnswer = (request: McpRequest, state: StateSeed | undefined) => AvProtocolResponse | Promise<AvProtocolResponse>;
|
|
17
|
+
/** The bound `@aventara/mcp` member each kind of MCP route is answered by. */
|
|
18
|
+
export declare const MCP_ROUTES: {
|
|
19
|
+
readonly [K in McpRoute["kind"]]: (binding: McpBinding) => McpRouteAnswer;
|
|
20
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export const PROTOCOL_ROUTES = {
|
|
2
|
-
operation: (binding) => (request) => binding.handleOperation(request),
|
|
2
|
+
operation: (binding) => (request, state) => binding.handleOperation(request, state === undefined ? {} : { state }),
|
|
3
3
|
contract: (binding) => (request) => binding.encodeContract(request),
|
|
4
|
-
transactions: (binding) => (request) => binding.handleTransaction(request),
|
|
4
|
+
transactions: (binding) => (request, state) => binding.handleTransaction(request, state === undefined ? {} : { state }),
|
|
5
|
+
};
|
|
6
|
+
export const MCP_ROUTES = {
|
|
7
|
+
endpoint: (binding) => (request, state) => binding.handleEndpoint(request, state === undefined ? {} : { state }),
|
|
8
|
+
metadata: (binding) => (request) => binding.encodeMetadata(request),
|
|
5
9
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/nest",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "NestJS host integration for Aventara.",
|
|
6
6
|
"type": "module",
|
|
@@ -22,24 +22,33 @@
|
|
|
22
22
|
"CHANGELOG.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@aventara/core": "1.0.0-rc.
|
|
25
|
+
"@aventara/core": "1.0.0-rc.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
+
"@aventara/mcp": "1.0.0-rc.1",
|
|
28
29
|
"@nestjs/common": "^12.0.0",
|
|
29
30
|
"@nestjs/core": "^12.0.0"
|
|
30
31
|
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@aventara/mcp": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
31
37
|
"publishConfig": {
|
|
32
38
|
"access": "public"
|
|
33
39
|
},
|
|
34
40
|
"devDependencies": {
|
|
35
|
-
"@aventara/
|
|
36
|
-
"@aventara/
|
|
41
|
+
"@aventara/mcp": "1.0.0-rc.1",
|
|
42
|
+
"@aventara/prisma7-adapter": "1.0.0-rc.1",
|
|
43
|
+
"@aventara/testing": "1.0.0-rc.1",
|
|
44
|
+
"@modelcontextprotocol/sdk": "1.30.1",
|
|
37
45
|
"@nestjs/common": "^12.1.2",
|
|
38
46
|
"@nestjs/core": "^12.1.2",
|
|
39
47
|
"@nestjs/platform-express": "^12.1.2",
|
|
40
48
|
"@nestjs/platform-fastify": "^12.1.2",
|
|
41
49
|
"@nestjs/testing": "^12.1.2",
|
|
42
50
|
"@prisma/adapter-better-sqlite3": "7.10.0",
|
|
51
|
+
"@prisma/adapter-pg": "7.10.0",
|
|
43
52
|
"reflect-metadata": "^0.2.2",
|
|
44
53
|
"rxjs": "^7.8.2"
|
|
45
54
|
},
|