lakutata 3.0.0 → 3.1.0

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.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/MCP.md ADDED
@@ -0,0 +1,293 @@
1
+ # Lakutata MCP
2
+
3
+ The Model Context Protocol (MCP) lets an AI assistant use an application: call its **tools**, read its **resources**
4
+ and fill its **prompts**. The package `@lakutata/mcp` serves controller methods as these, over the standard input and
5
+ output of a child process or over HTTP. It is not part of `lakutata`:
6
+
7
+ ```bash
8
+ npm install lakutata @lakutata/mcp
9
+ ```
10
+
11
+ | Declared with | What it is | Who uses it |
12
+ |---|---|---|
13
+ | `@MCPTool(name)` | An action the model calls with arguments | The model, when it decides to |
14
+ | `@MCPResource(uri)` | Data read by its URI | The client, to give it to the model |
15
+ | `@MCPPrompt(name)` | A message template filled with arguments | The user, by choosing it in the client |
16
+
17
+ ## 1. A server over stdio
18
+
19
+ ```typescript
20
+ import {Application, DTO, Exception} from 'lakutata'
21
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
22
+ import {Prefix} from 'lakutata/decorator/ctrl'
23
+ import {Expect} from 'lakutata/decorator/dto'
24
+ import {Logger} from 'lakutata/com/logger'
25
+ import {MCPPrompt, MCPResource, MCPTool, stdioMCPEntrypoint} from '@lakutata/mcp'
26
+
27
+ class DeviceSnDTO extends DTO {
28
+ @Expect(DTO.String().required().description('The serial number of the device'))
29
+ public sn: string
30
+ }
31
+
32
+ class StatusDTO extends DTO {
33
+ @Expect(DTO.String().required())
34
+ public sn: string
35
+
36
+ @Expect(DTO.Boolean().required())
37
+ public online: boolean
38
+ }
39
+
40
+ class OfflineException extends Exception {
41
+ public errno: string = 'E_OFFLINE'
42
+ }
43
+
44
+ @Prefix({mcp: 'device'})
45
+ class DeviceController extends Controller {
46
+ //The tool device_status
47
+ @MCPTool('status', DeviceSnDTO, {description: 'The online status of a device', readOnly: true, output: StatusDTO})
48
+ public async status(input: DeviceSnDTO): Promise<StatusDTO> {
49
+ return {sn: input.sn, online: input.sn !== 'off'} as StatusDTO
50
+ }
51
+
52
+ @MCPTool('reboot', DeviceSnDTO, {description: 'Reboot a device', destructive: true})
53
+ public async reboot(input: DeviceSnDTO): Promise<string> {
54
+ if (input.sn === 'off') throw new OfflineException('The device {sn} is offline', {sn: input.sn})
55
+ return `Rebooting ${input.sn}`
56
+ }
57
+
58
+ //The resource of each device: device://A1/status
59
+ @MCPResource('device://{sn}/status', DeviceSnDTO, {mimeType: 'application/json', description: 'The status of a device'})
60
+ public async statusResource(input: DeviceSnDTO): Promise<StatusDTO> {
61
+ return {sn: input.sn, online: true} as StatusDTO
62
+ }
63
+
64
+ //The prompt device_diagnose
65
+ @MCPPrompt('diagnose', DeviceSnDTO, {description: 'Diagnose the failures of a device'})
66
+ public async diagnose(input: DeviceSnDTO): Promise<string> {
67
+ return `Diagnose the device ${input.sn} from its status and its logs.`
68
+ }
69
+ }
70
+
71
+ Application.run(() => ({
72
+ id: 'devices.app',
73
+ name: 'Devices',
74
+ components: {
75
+ //The standard output belongs to the protocol: the logs go to the standard error
76
+ log: {class: Logger, destinations: [process.stderr]},
77
+ entrypoint: buildEntrypoints({
78
+ controllers: [DeviceController],
79
+ mcp: stdioMCPEntrypoint({instructions: 'Check the status of a device before rebooting it.'})
80
+ })
81
+ },
82
+ bootstrap: ['entrypoint']
83
+ }))
84
+ ```
85
+
86
+ - The client runs the application as its child process, with a command such as `node build/App.js`, and speaks to it
87
+ on its standard input and output. The application exits once the client closes its input (`exitOnClose: false`
88
+ keeps it running).
89
+ - The standard output carries the messages of the protocol: write the logs to the standard error, as above, and never
90
+ call `console.log()`.
91
+ - The options of `stdioMCPEntrypoint()`: `serverName` (the name of the application by default), `serverVersion`
92
+ (`'1.0.0'`), `instructions` for the model, `stdin` and `stdout`, `exitOnClose`, `name` and `middlewares`.
93
+ - Over stdio, the server speaks the revisions of 2025 of the protocol.
94
+
95
+ ## 2. Tools
96
+
97
+ ```text
98
+ @MCPTool(name, DTO?, options?)
99
+ ```
100
+
101
+ | Option | Meaning |
102
+ |---|---|
103
+ | `description` | What the tool does, for the model; without it, the model sees `ControllerClass:method` |
104
+ | `title` | A name to display |
105
+ | `readOnly`, `destructive`, `idempotent`, `openWorld` | Hints for the client: the tool changes nothing, makes changes that cannot be undone, gives the same result when called again, or reaches outside systems |
106
+ | `output` | The DTO of the result: its JSON schema is sent, and the result is given as structured content too |
107
+ | `acl`, `rule`, `middlewares`… | The options of every action (see [Controllers](Controllers.md), section 3) |
108
+
109
+ - A name has 1 to 128 letters, digits, `_`, `-` or `.`. `@Prefix({mcp: 'device'})` prefixes the names of the tools and
110
+ of the prompts of a controller: `status` becomes `device_status`.
111
+ - The arguments are described by the JSON schema of the DTO; `.description()` describes an argument to the model. The
112
+ DTO validates them, as for every action.
113
+ - A tool declared by two controllers is served by the one registered last, as on the other transports.
114
+
115
+ What a tool returns becomes its result:
116
+
117
+ | Returned | Result |
118
+ |---|---|
119
+ | A string | One text |
120
+ | An object | Its JSON as a text, and the object as structured content |
121
+ | `undefined` | No content |
122
+ | `Result.file(buffer, {contentType})` | An image or an audio for these types, otherwise an embedded resource |
123
+ | `Result.status(status, data)` | Its data |
124
+
125
+ - A class instance (a DTO) is given as structured content only when the tool declares `output`. A tool that declares
126
+ `output` and returns no object fails.
127
+ - The server does not check the structured content against `output`; the client does, and refuses a result that does
128
+ not match.
129
+ - An error thrown by the tool, its validation or its access control included, is given to the model as a result
130
+ marked as an error, whose text is `errno: message` (`E_OFFLINE: The device off is offline`). The model can then
131
+ correct its call.
132
+
133
+ ## 3. Resources
134
+
135
+ ```text
136
+ @MCPResource(uri, DTO?, options?)
137
+ ```
138
+
139
+ - A URI without variables (`device://catalog`) is a resource of its own; a URI with variables
140
+ (`device://{sn}/status`) is a template, which the client fills. The URIs are never prefixed.
141
+ - The variables are the input of the action. They are strings: declare them as strings, or convert them with
142
+ `.strict(false)`, as the query of an HTTP request.
143
+ - The options: `resourceName` (the name listed; the name of the action by default), `title`, `mimeType`, `audience`
144
+ (`['user']`, `['assistant']`), `priority` (from 0 to 1), and the options of every action.
145
+
146
+ | Returned | Content |
147
+ |---|---|
148
+ | A string | A text, of the `mimeType` (`text/plain` by default) |
149
+ | Another value | Its JSON, of the `mimeType` (`application/json` by default) |
150
+ | A `Buffer`, `Result.file()` | A text or a binary, by its type |
151
+
152
+ An error thrown when a resource is read is answered as an error of the protocol: invalid params (`-32602`) when its
153
+ `statusCode` is a client error (400 to 499, an invalid input among them), internal error (`-32603`) otherwise. Its
154
+ message is `errno: message`.
155
+
156
+ ## 4. Prompts
157
+
158
+ ```text
159
+ @MCPPrompt(name, DTO?, options?)
160
+ ```
161
+
162
+ - The properties of the DTO are the arguments of the prompt, listed to the client. They are strings: declare them as
163
+ strings, or convert them with `.strict(false)`.
164
+ - A string becomes one message of the user; an array, one message per item (a string, or `{role, content}`); an object
165
+ `{messages}` is sent as it is.
166
+ - The option `title`, and the options of every action. The errors are answered as for the resources.
167
+
168
+ ## 5. The context
169
+
170
+ In an MCP action, `this.context` is an `MCPContext`:
171
+
172
+ | Property | What it is |
173
+ |---|---|
174
+ | `kind` | `'tool'`, `'resource'` or `'prompt'` |
175
+ | `name` | The name of the tool or of the prompt, prefix included; for a resource, its URI as declared |
176
+ | `uri` | The URI read, for a resource |
177
+ | `data` | The arguments, before the validation (empty for a resource, whose variables are its input) |
178
+ | `authInfo` | What `authenticate` returned (section 6) |
179
+ | `progress(progress, total?, message?)` | Reports the progress of a long call to the client |
180
+
181
+ ```typescript
182
+ import {Controller} from 'lakutata/com/entrypoint'
183
+ import {MCPTool, type MCPContext} from '@lakutata/mcp'
184
+
185
+ class ImportController extends Controller {
186
+ @MCPTool('import', {description: 'Import the devices'})
187
+ public async import(): Promise<string> {
188
+ const context: MCPContext = this.context as MCPContext
189
+ for (let step: number = 1; step <= 3; step++) {
190
+ //The client cancelled the call
191
+ if (this.signal?.aborted) return 'cancelled'
192
+ await context.progress(step, 3, `step ${step}`)
193
+ }
194
+ return 'imported'
195
+ }
196
+ }
197
+ ```
198
+
199
+ - When the client cancels a call, `this.signal` is aborted.
200
+ - A method may serve several transports at once (`@GET` and `@MCPTool`): `this.context.type` is then `'MCP'`
201
+ (`MCP_CONTEXT_TYPE`) for the MCP requests.
202
+ - The access rules of the entrypoint apply to the MCP actions declared with `acl: true` when their
203
+ `supportContextTypes()` include `MCP_CONTEXT_TYPE` (see [Middlewares](Middlewares.md)).
204
+ - The server does not ask the client for anything: there is no sampling, elicitation or logging to the client.
205
+
206
+ **Notifications.** `MCPNotifier`, a component to inject (`@Inject(MCPNotifier)`), tells the clients that something
207
+ changed: `resourceUpdated(uri)`, `resourcesChanged()`, `toolsChanged()`, `promptsChanged()`. Over stdio, a resource
208
+ update reaches the client only if it subscribed to that URI. Over HTTP, only the clients of the revision 2026-07-28
209
+ that listen to the changes are notified.
210
+
211
+ ## 6. Over HTTP
212
+
213
+ ```typescript
214
+ import type {IncomingMessage} from 'node:http'
215
+ import {Application} from 'lakutata'
216
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
217
+ import {httpMCPEntrypoint, MCPTool, type MCPAuthInfo} from '@lakutata/mcp'
218
+
219
+ class ClockController extends Controller {
220
+ @MCPTool('now', {description: 'The current time of the server', readOnly: true})
221
+ public async now(): Promise<string> {
222
+ return new Date().toISOString()
223
+ }
224
+ }
225
+
226
+ //Throwing answers 401
227
+ function verifyToken(request: IncomingMessage): MCPAuthInfo {
228
+ const token: string = request.headers.authorization?.replace(/^Bearer /, '') ?? ''
229
+ if (token !== process.env.MCP_TOKEN) throw new Error('Invalid token')
230
+ return {token: token, clientId: 'assistant', scopes: []}
231
+ }
232
+
233
+ //The clients connect to http://127.0.0.1:3000/mcp
234
+ Application.run(() => ({
235
+ id: 'clock.app',
236
+ name: 'Clock',
237
+ components: {
238
+ entrypoint: buildEntrypoints({
239
+ controllers: [ClockController],
240
+ mcp: httpMCPEntrypoint({port: 3000, authenticate: verifyToken})
241
+ })
242
+ },
243
+ bootstrap: ['entrypoint']
244
+ }))
245
+ ```
246
+
247
+ | Option | Default | Meaning |
248
+ |---|---|---|
249
+ | `port`, `host`, `path` | `3000`, `'127.0.0.1'`, `'/mcp'` | Where it listens |
250
+ | `authenticate` | | `(request) => MCPAuthInfo \| undefined`: the authentication of the client; throwing answers 401, returning `undefined` serves the request without one |
251
+ | `allowedHosts`, `allowedOrigins` | the local names, on a local address | The `Host` and `Origin` headers accepted (403 otherwise); `false` checks none |
252
+ | `legacy` | `'stateless'` | The clients of the revisions of 2025: served without sessions, or refused with `'reject'` |
253
+ | `maxRequestBodySize` | 4 MiB | A larger body is answered with 413 |
254
+ | `bus` | in memory | Where the notifications go, to share them between the instances of the application |
255
+ | `listen` | `true` | `false` opens no port: the `handler` is mounted on another server |
256
+ | `serverName`, `serverVersion`, `instructions`, `server`, `listening`, `name`, `middlewares` | | As for stdio; `server` holds the options of the `node:http` server |
257
+
258
+ - It serves the protocol over Streamable HTTP: the revision 2026-07-28, and the revisions of 2025 without sessions.
259
+ - On a local address, the `Host` and `Origin` headers are checked against the local names, which protects the server
260
+ from DNS rebinding: behind a proxy, list the names the clients use. On another address, nothing is checked unless you
261
+ list names.
262
+ - When the application stops, the calls in progress are answered, then the port closes.
263
+
264
+ **Sharing the port of the HTTP adapter.** With `listen: false`, mount the `handler` of the adapter on the Express
265
+ application, in its `before` hook:
266
+
267
+ ```typescript
268
+ import {buildEntrypoints} from 'lakutata/com/entrypoint'
269
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
270
+ import {httpMCPEntrypoint} from '@lakutata/mcp'
271
+
272
+ //The names the clients connect with (http://localhost:8080/mcp)
273
+ const mcp = httpMCPEntrypoint({listen: false, allowedHosts: ['localhost', '127.0.0.1', 'mcp.example.com']})
274
+
275
+ export const entrypoint = buildEntrypoints({
276
+ controllers: [],
277
+ http: expressHTTPEntrypoint({port: 8080, before: (app): void => void app.all('/mcp', mcp.handler)}),
278
+ mcp: mcp
279
+ })
280
+ ```
281
+
282
+ A mounted handler checks the `Host` and `Origin` headers only against the names given, and answers 503 before the
283
+ adapter starts and once it stops.
284
+
285
+ ## 7. Errors
286
+
287
+ | Situation | The client receives |
288
+ |---|---|
289
+ | A tool fails: an exception, an invalid argument, a refused access | A result marked as an error: `errno: message` |
290
+ | A resource or a prompt fails with a client error (400 to 499) | The protocol error `-32602`, its message `errno: message`, its data `{errno}` |
291
+ | A resource or a prompt fails otherwise | The protocol error `-32603` |
292
+ | An unknown tool, prompt or resource | The protocol error `-32602` (`Tool nope not found`) |
293
+ | An invalid name of a tool or a prompt, a resource URI without scheme | A `TypeError` when the class is declared |
@@ -0,0 +1,213 @@
1
+ # Lakutata Middlewares and Access Control
2
+
3
+ A middleware runs around the actions: it sees each request before the action, and its result after. The access control
4
+ decides whether a request may reach its action. Both work the same way for every transport.
5
+
6
+ ## 1. Middlewares
7
+
8
+ A middleware is a function `(invocation, next) => Promise<unknown>`. It calls `next()` to go on with the request, and
9
+ returns what the request returns:
10
+
11
+ ```typescript
12
+ import {type ActionInvocation, Controller, type Next} from 'lakutata/com/entrypoint'
13
+ import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
14
+
15
+ async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
16
+ const started: number = Date.now()
17
+ try {
18
+ return await next()
19
+ } finally {
20
+ console.log(`${invocation.action.name} took ${Date.now() - started} ms`)
21
+ }
22
+ }
23
+
24
+ @UseMiddlewares(timing)
25
+ class HealthController extends Controller {
26
+ @GET('/health')
27
+ public async health(): Promise<string> {
28
+ return 'ok'
29
+ }
30
+ }
31
+ ```
32
+
33
+ A middleware can also be a class that extends `Middleware` and implements `handle(invocation, next)`. The class is
34
+ created for each request, in the scope of the request, so it can inject other objects:
35
+
36
+ ```typescript
37
+ import {Component} from 'lakutata'
38
+ import {type ActionInvocation, Middleware, type Next} from 'lakutata/com/entrypoint'
39
+ import {Inject} from 'lakutata/decorator/di'
40
+
41
+ class Metrics extends Component {
42
+ public record(name: string, milliseconds: number): void {
43
+ console.log(name, milliseconds)
44
+ }
45
+ }
46
+
47
+ export class Timing extends Middleware {
48
+ //The component declared as metrics in the components of the application
49
+ @Inject('metrics')
50
+ protected readonly metrics: Metrics
51
+
52
+ public async handle(invocation: ActionInvocation, next: Next): Promise<unknown> {
53
+ const started: number = Date.now()
54
+ try {
55
+ return await next()
56
+ } finally {
57
+ this.metrics.record(invocation.action.name, Date.now() - started)
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ `invocation` describes the request:
64
+
65
+ | Property | Meaning |
66
+ |---|---|
67
+ | `context` | The context of the request (`this.context` in the action). Its `data`, the input before validation, can be changed |
68
+ | `action` | The action: its `name`, `description`, `groups`, its address (`route`, `command`, `pattern`) |
69
+ | `container` | The container of the request's scope, to get the objects of the request |
70
+ | `signal` | The `AbortSignal` of the request |
71
+
72
+ - A middleware that returns without calling `next()` answers the request itself: the action does not run. A cache
73
+ can work this way.
74
+ - A middleware that throws fails the request with its error, as an action that throws.
75
+ - `next()` can be called only once: a second call throws a `MiddlewareNextCalledException`.
76
+ - The middlewares run for every transport, the scheduled jobs included. `invocation.context.type` tells them apart.
77
+
78
+ ## 2. Where to declare them
79
+
80
+ | Where | Applies to |
81
+ |---|---|
82
+ | `buildEntrypoints({middlewares})` | Every action |
83
+ | The `middlewares` option of an adapter | The actions reached through that adapter |
84
+ | `@UseMiddlewares(...)` on a controller class | The actions of the class and of its subclasses |
85
+ | The `middlewares` action option | That action |
86
+ | `@UseMiddlewares(...)` on a method | That method |
87
+
88
+ They run in the order of this table, from the first row (the outermost) to the last: entrypoint, adapter, controller
89
+ class, action option, method. On a controller, the middlewares of the parent classes run before the ones of the
90
+ subclass. After the last middleware come the validation of the input, the access control, then the action.
91
+
92
+ ## 3. Passing data to the action
93
+
94
+ A middleware often finds something the action needs, such as the user who sent the request. The input is the wrong
95
+ place for it: the DTO validates the input after the middlewares, and removes the keys it does not declare. Use a
96
+ provider of the request instead. A `@Scoped` provider has one instance per request: the middleware fills it, and the
97
+ action injects the same instance.
98
+
99
+ ```typescript
100
+ import {Exception, Provider} from 'lakutata'
101
+ import {type ActionInvocation, Controller, type HTTPContext, Middleware, type Next} from 'lakutata/com/entrypoint'
102
+ import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
103
+ import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
104
+
105
+ class UnauthorizedException extends Exception {
106
+ public errno: string = 'E_UNAUTHORIZED'
107
+
108
+ public statusCode: number = 401
109
+ }
110
+
111
+ //One instance per request, registered the first time it is used
112
+ @Autoload()
113
+ @Scoped()
114
+ export class CurrentUser extends Provider {
115
+ public name: string
116
+
117
+ public role: string
118
+ }
119
+
120
+ class Authenticate extends Middleware {
121
+ public async handle(invocation: ActionInvocation, next: Next): Promise<unknown> {
122
+ const token: string | undefined = (invocation.context as HTTPContext).request.headers.authorization
123
+ if (!token) throw new UnauthorizedException('Not logged in')
124
+ const user: CurrentUser = await invocation.container.get(CurrentUser)
125
+ //Look the token up in a real application
126
+ user.name = token
127
+ user.role = token === 'ada' ? 'admin' : 'user'
128
+ return await next()
129
+ }
130
+ }
131
+
132
+ @UseMiddlewares(Authenticate)
133
+ class ProfileController extends Controller {
134
+ @Inject(CurrentUser)
135
+ protected readonly user: CurrentUser
136
+
137
+ @GET('/me')
138
+ public async me(): Promise<{ name: string, role: string }> {
139
+ return {name: this.user.name, role: this.user.role}
140
+ }
141
+ }
142
+ ```
143
+
144
+ ```text
145
+ $ curl localhost:8080/me -H 'Authorization: ada'
146
+ {"name":"ada","role":"admin"}
147
+ $ curl localhost:8080/me
148
+ {"errno":"E_UNAUTHORIZED","errMsg":"Not logged in"}
149
+ ```
150
+
151
+ ## 4. Access control
152
+
153
+ An action declared with `acl: true` is checked by the access rules of the entrypoint. A rule is a class that extends
154
+ `AccessControlRule` and is listed in `buildEntrypoints({rules})`:
155
+
156
+ ```typescript
157
+ import {Provider} from 'lakutata'
158
+ import {AccessControlRule, ContextType, Controller, type HTTPContext} from 'lakutata/com/entrypoint'
159
+ import {DELETE, GET} from 'lakutata/decorator/ctrl'
160
+ import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
161
+
162
+ //The provider of section 3, filled by the middleware Authenticate
163
+ @Autoload()
164
+ @Scoped()
165
+ class CurrentUser extends Provider {
166
+ public name: string
167
+
168
+ public role: string
169
+ }
170
+
171
+ //The actions of the "admin" group are for the administrators
172
+ export class AdminRule extends AccessControlRule<HTTPContext> {
173
+ @Inject(CurrentUser)
174
+ protected readonly user: CurrentUser
175
+
176
+ public get supportContextTypes(): ContextType[] {
177
+ return [ContextType.HTTP]
178
+ }
179
+
180
+ public async rule(): Promise<boolean> {
181
+ if (!this.targetAction.groups.includes('admin')) return true
182
+ return this.user.role === 'admin'
183
+ }
184
+ }
185
+
186
+ export class StatsController extends Controller {
187
+ @GET('/admin/stats', {acl: true, groups: ['admin']})
188
+ public async stats(): Promise<string> {
189
+ return 'stats'
190
+ }
191
+
192
+ //A rule of this action only; no acl needed
193
+ @DELETE('/drafts/:id', {rule: async (context): Promise<boolean> => (context as HTTPContext).request.headers['x-owner'] === 'yes'})
194
+ public async removeDraft(): Promise<void> {
195
+ }
196
+ }
197
+
198
+ //buildEntrypoints({controllers: [StatsController], rules: [AdminRule], httpActionGroups: {admin: 'Administration'}})
199
+ ```
200
+
201
+ - A rule is created for each request, in the scope of the request: it can inject the objects of the request, such as
202
+ the `CurrentUser` filled by a middleware.
203
+ - In a rule, `this.context` is the context of the request, `this.input` the validated input, `this.targetAction` the
204
+ action (its `name`, its `groups`), and `this.actions` all the actions of the transport.
205
+ - `supportContextTypes` lists the transports the rule checks; it is skipped for the others.
206
+ - The rules run in the order of the `rules` option, then the `rule` option of the action. The first one that resolves
207
+ `false` denies the request with an `AccessDeniedException` (errno `E_ACCESS_DENY`, status 403 over HTTP); one that
208
+ throws denies it with its own error (such as a 401).
209
+ - The `rule` option of an action works on its own: without `acl`, only that rule is checked.
210
+ - An action with neither `acl` nor `rule` is not checked. An `acl: true` action is allowed when no rule denies it.
211
+
212
+ The access control runs after the middlewares and the validation of the input, just before the action. To change how
213
+ it decides for the whole application, see [Entrypoint](Entrypoint.md), section 7.