@andrewcaires/api 5.7.1 → 5.8.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.
package/README.md CHANGED
@@ -13,33 +13,101 @@ The module is now available on npm! `npm i @andrewcaires/api`
13
13
 
14
14
  ## Example usage
15
15
 
16
- ```js
17
- // .env
16
+ ### Environment
17
+
18
+ Copy `.env.example` to `.env` and replace every value enclosed in angle brackets. The values below are intended for local development; use a secret manager in staging and production.
18
19
 
20
+ ```dotenv
21
+ # Runtime
19
22
  NODE_ENV=development
23
+ API_NAME=api
24
+ CONSOLE_ERROR=true
25
+
26
+ # Audit
27
+ AUDIT_IGNORE=hash,password
28
+ DATABASE_AUDIT=true
20
29
 
21
- DATABASE_URI=mariadb://root:root@localhost:3306/database
30
+ # Database
31
+ DATABASE_URI=mariadb://api:api@127.0.0.1:3306/api
22
32
  DATABASE_LOG=false
33
+ DATABASE_PREFIX=
34
+ DATABASE_TIMEZONE=+00:00
23
35
 
24
- HTTP_PORT=3000
36
+ # Encryption
37
+ # Generate with: openssl rand -base64 32
38
+ ENCRYPTION_SECRET=<base64-encoded-32-byte-key>
39
+
40
+ # HTTP
41
+ HTTP_CROSS=http://localhost:5173
42
+ HTTP_HYBRID=false
43
+ HTTP_NOTFOUND=
25
44
  HTTP_PATH=/api
26
- HTTP_PUBLIC=/public
27
- HTTP_CROSS=true
45
+ HTTP_PORT=3000
46
+ HTTP_PUBLIC=./public
47
+ HTTP_REDIRECT=
28
48
 
29
- // HTTP_TLS_CRT=./ssl/http.crt
30
- // HTTP_TLS_KEY=./ssl/http.key
49
+ # TLS (optional)
50
+ # HTTP_TLS_CA=./ssl/ca.pem
51
+ # HTTP_TLS_CRT=./ssl/certificate.pem
52
+ # HTTP_TLS_KEY=./ssl/private-key.pem
31
53
 
32
- TOKEN_SECRET=change-me-to-a-long-random-secret
33
- TOKEN_TTL=1d
34
- TOKEN_TTL_RENEW=1d
54
+ # Models
55
+ MODEL_LIMIT=10
56
+ MODEL_LIMIT_MAX=100
57
+
58
+ # MCP
59
+ MCP_ALLOWED_HOSTS=localhost,127.0.0.1,[::1]
60
+ MCP_ALLOWED_ORIGINS=localhost,127.0.0.1,[::1]
61
+ MCP_MAX_REQUEST_BODY_SIZE=4194304
62
+
63
+ # Password hashing
64
+ PASSWORD_ALGORITHM=2
65
+ PASSWORD_MEMORY_COST=4096
66
+ PASSWORD_PARALLELISM=1
67
+ PASSWORD_PEPPER=<random-password-pepper>
68
+ PASSWORD_REUSE=false
69
+ PASSWORD_TIME_COST=3
70
+
71
+ # Request delays
72
+ SLEEP_AUTH=2s
73
+ SLEEP_FORGOT=5s
74
+ SLEEP_REGISTRY=5s
75
+
76
+ # Tenant
77
+ TENANT_HEADER=X-Tenant-ID
78
+
79
+ # Tokens and API keys
35
80
  TOKEN_COOKIE=authorization
36
81
  TOKEN_COOKIE_SET=true
37
82
  TOKEN_HEADER=authorization
83
+ TOKEN_PEPPER=<random-token-pepper>
84
+ TOKEN_PREFIX=api
85
+ TOKEN_QUERY=api_key
86
+ TOKEN_SECRET=<random-token-secret>
87
+ TOKEN_TTL=1d
88
+ TOKEN_TTL_FORGOT=1d
89
+ TOKEN_TTL_RENEW=1d
38
90
  TOKEN_TYPE=bearer
39
91
 
92
+ # Request body
93
+ API_BODYPARSER_STRICT=true
94
+ API_BODYPARSER_INFLATE=true
95
+ API_BODYPARSER_LIMIT=10mb
96
+ API_BODYPARSER_TYPE=json
97
+
98
+ # Sessions
99
+ API_SESSION_NAME=session.id
100
+ API_SESSION_LIFETIME=1d
101
+ API_SESSION_SECRET=<random-session-secret>
102
+
103
+ # WebSocket
40
104
  API_WEBSOCKET_START=false
41
105
  ```
42
106
 
107
+ `DATABASE_URI` is required. Replace every secret placeholder with an independently generated value; never reuse the token, password, encryption, or session secrets. Keep `.env` out of version control. `SETUP_ADMIN_PASSWORD` is intentionally omitted because it should only be injected into the one-shot initial setup process.
108
+
109
+ ### Application
110
+
43
111
  ```js
44
112
  // index.ts
45
113
 
@@ -80,6 +148,294 @@ const main = async () => {
80
148
  main().catch(console.log);
81
149
  ```
82
150
 
151
+ ## HTTP endpoints
152
+
153
+ The paths below use the default `HTTP_PATH=/api`. Replace `/api` with the configured value when a different HTTP prefix is used. Only controllers passed to `Application` are exposed.
154
+
155
+ `Authenticated` means the route uses `@IsAuthenticated`. Most authenticated routes also require action, scope, or resource permissions through decorators such as `CanRead`, `CanCreate`, `CanUpdate`, `CanDelete`, `HasScope`, or `HasPermissions`.
156
+
157
+ ### Audits
158
+
159
+ | Method | Path | Access |
160
+ | --- | --- | --- |
161
+ | `GET` | `/api/audits` | Authenticated |
162
+ | `GET` | `/api/audits/record/:record` | Authenticated |
163
+ | `GET` | `/api/audits/:id` | Authenticated |
164
+
165
+ ### Authentication
166
+
167
+ | Method | Path | Access |
168
+ | --- | --- | --- |
169
+ | `POST` | `/api/auth/login` | Public |
170
+ | `GET` | `/api/auth/logout` | Authenticated |
171
+ | `GET` | `/api/auth/me` | Authenticated |
172
+ | `POST` | `/api/auth/renew` | Public |
173
+
174
+ ### Files
175
+
176
+ | Method | Path | Access |
177
+ | --- | --- | --- |
178
+ | `GET` | `/api/files/path` | Authenticated |
179
+ | `GET` | `/api/files/path/*path` | Authenticated |
180
+
181
+ ### Password recovery
182
+
183
+ | Method | Path | Access |
184
+ | --- | --- | --- |
185
+ | `GET` | `/api/forgot/check` | Public |
186
+ | `POST` | `/api/forgot` | Public |
187
+ | `POST` | `/api/forgot/reset` | Public |
188
+
189
+ ### Groups
190
+
191
+ | Method | Path | Access |
192
+ | --- | --- | --- |
193
+ | `POST` | `/api/groups` | Authenticated |
194
+ | `GET` | `/api/groups` | Authenticated |
195
+ | `GET` | `/api/groups/count` | Authenticated |
196
+ | `GET` | `/api/groups/:id` | Authenticated |
197
+ | `PUT` | `/api/groups/:id` | Authenticated |
198
+ | `DELETE` | `/api/groups/:id` | Authenticated |
199
+ | `GET` | `/api/groups/:id/users` | Authenticated |
200
+ | `GET` | `/api/groups/:id/permissions` | Authenticated |
201
+ | `POST` | `/api/groups/:id/roles` | Authenticated |
202
+ | `GET` | `/api/groups/:id/roles` | Authenticated |
203
+ | `PUT` | `/api/groups/:id/roles` | Authenticated |
204
+ | `DELETE` | `/api/groups/:id/roles` | Authenticated |
205
+
206
+ ### Health check
207
+
208
+ | Method | Path | Access |
209
+ | --- | --- | --- |
210
+ | `ALL` | `/api/health/` | Public |
211
+
212
+ `ALL` registers the health check for every HTTP method.
213
+
214
+ ### Logs
215
+
216
+ | Method | Path | Access |
217
+ | --- | --- | --- |
218
+ | `GET` | `/api/logs` | Authenticated |
219
+
220
+ ### MCP
221
+
222
+ | Method | Path | Access |
223
+ | --- | --- | --- |
224
+ | `ALL` | `/api/mcp` | Bearer-authenticated, except CORS preflight |
225
+ | `ALL` | `/api/mcp/public` | Public |
226
+
227
+ Both endpoints use the official Model Context Protocol TypeScript SDK v2 with a stateless server created for each request. Register `McpController` alongside the application controllers that expose MCP tools. The authenticated endpoint includes every discovered tool; the public endpoint includes only tools explicitly declared with `public: true`.
228
+
229
+ Every request to `/api/mcp` is authenticated independently with `Authorization: Bearer <token>`. Application sessions are not reused as MCP sessions, no `MCP-Session-Id` is persisted, and no MCP session TTL needs to be configured. `/api/mcp/public` does not accept authentication as a substitute for tool visibility; its catalog is restricted at server creation.
230
+
231
+ `HEAD` requests return `204 No Content` for compatibility with connectivity checks. The authenticated endpoint still requires a valid Bearer token; the public endpoint does not. These requests do not initialize an MCP session or invoke a tool.
232
+
233
+ When the authenticated user belongs to one or more tenants, the request must also send `X-Tenant-ID` (or the configured `TENANT_HEADER`). Permissions are loaded only for that tenant; handlers should use `context.principal.tenantId` as their tenant boundary.
234
+
235
+ `MCP_ALLOWED_HOSTS` and `MCP_ALLOWED_ORIGINS` are comma-separated hostname allowlists without scheme or port. Both default to `localhost,127.0.0.1,[::1]`. `MCP_MAX_REQUEST_BODY_SIZE` limits MCP request bodies in bytes and defaults to `4194304` (4 MiB).
236
+
237
+ Clients should send `Content-Type: application/json` and advertise both supported response formats with `Accept: application/json, text/event-stream`. Use an official MCP client instead of managing protocol initialization or transport headers manually. See the official [MCP Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) and [TypeScript SDK HTTP documentation](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md).
238
+
239
+ #### Tool permissions
240
+
241
+ Tools are private by default. Set `public: true` only when a tool can be safely discovered and called without an authenticated identity:
242
+
243
+ ```ts
244
+ @McpTool({
245
+ name: "status",
246
+ title: "Service status",
247
+ description: "Returns public service availability.",
248
+ public: true,
249
+ input: {},
250
+ output: {
251
+ available: Validation.boolean(),
252
+ },
253
+ annotations: {
254
+ readOnlyHint: true,
255
+ destructiveHint: false,
256
+ },
257
+ })
258
+ protected async status() {
259
+
260
+ return { available: true };
261
+ }
262
+ ```
263
+
264
+ Public tools are available at both `/api/mcp/public` and the authenticated `/api/mcp` endpoint. A public tool cannot declare `requiredPermissions`; this ambiguous configuration fails during startup. Omitting both options keeps the tool authenticated without requiring a specific application permission.
265
+
266
+ Declare the application permissions required to call a tool with `requiredPermissions`:
267
+
268
+ ```ts
269
+ @McpTool({
270
+ name: "tickets.find",
271
+ title: "Find tickets",
272
+ description: "Finds tickets in the selected tenant.",
273
+ input: {},
274
+ requiredPermissions: ["tickets.read"],
275
+ })
276
+ protected async findTickets(_args: TypeAnyObject, context: McpToolContext) {
277
+
278
+ return this.ticketService.find({ tenantId: context.principal?.tenantId });
279
+ }
280
+ ```
281
+
282
+ All declared permissions are required. An empty or omitted array allows every authenticated MCP principal to call the tool. Missing permissions return an MCP tool error without invoking the decorated method.
283
+
284
+ `McpController` resolves permissions from the authenticated user's active roles and groups for the selected tenant. Applications with tenants must also send the configured tenant header, `X-Tenant-ID` by default. The application principal remains authoritative for permission checks.
285
+
286
+ ### Pages
287
+
288
+ | Method | Path | Access |
289
+ | --- | --- | --- |
290
+ | `POST` | `/api/pages` | Authenticated |
291
+ | `GET` | `/api/pages` | Authenticated |
292
+ | `GET` | `/api/pages/count` | Authenticated |
293
+ | `GET` | `/api/pages/:id` | Authenticated |
294
+ | `PUT` | `/api/pages/:id` | Authenticated |
295
+ | `DELETE` | `/api/pages/:id` | Authenticated |
296
+ | `PUT` | `/api/pages/position` | Authenticated |
297
+
298
+ ### Permissions
299
+
300
+ | Method | Path | Access |
301
+ | --- | --- | --- |
302
+ | `POST` | `/api/permissions` | Authenticated |
303
+ | `GET` | `/api/permissions` | Authenticated |
304
+ | `GET` | `/api/permissions/count` | Authenticated |
305
+ | `GET` | `/api/permissions/:id` | Authenticated |
306
+ | `PUT` | `/api/permissions/:id` | Authenticated |
307
+ | `DELETE` | `/api/permissions/:id` | Authenticated |
308
+ | `GET` | `/api/permissions/:id/groups` | Authenticated |
309
+
310
+ ### Profile
311
+
312
+ | Method | Path | Access |
313
+ | --- | --- | --- |
314
+ | `GET` | `/api/profile` | Authenticated |
315
+ | `POST` | `/api/profile/avatar` | Authenticated |
316
+ | `DELETE` | `/api/profile/avatar` | Authenticated |
317
+ | `POST` | `/api/profile/password` | Authenticated |
318
+
319
+ ### Registration
320
+
321
+ | Method | Path | Access |
322
+ | --- | --- | --- |
323
+ | `POST` | `/api/register` | Public |
324
+ | `POST` | `/api/register/confirm` | Public |
325
+ | `POST` | `/api/register/send` | Public |
326
+
327
+ ### Roles
328
+
329
+ | Method | Path | Access |
330
+ | --- | --- | --- |
331
+ | `POST` | `/api/roles` | Authenticated |
332
+ | `GET` | `/api/roles` | Authenticated |
333
+ | `GET` | `/api/roles/count` | Authenticated |
334
+ | `GET` | `/api/roles/:id` | Authenticated |
335
+ | `PUT` | `/api/roles/:id` | Authenticated |
336
+ | `DELETE` | `/api/roles/:id` | Authenticated |
337
+ | `GET` | `/api/roles/:id/groups` | Authenticated |
338
+ | `POST` | `/api/roles/:id/permissions` | Authenticated |
339
+ | `GET` | `/api/roles/:id/permissions` | Authenticated |
340
+ | `PUT` | `/api/roles/:id/permissions` | Authenticated |
341
+ | `DELETE` | `/api/roles/:id/permissions` | Authenticated |
342
+
343
+ ### Tenants
344
+
345
+ | Method | Path | Access |
346
+ | --- | --- | --- |
347
+ | `POST` | `/api/tenants` | Authenticated |
348
+ | `GET` | `/api/tenants` | Authenticated |
349
+ | `GET` | `/api/tenants/count` | Authenticated |
350
+ | `GET` | `/api/tenants/:id` | Authenticated |
351
+ | `PUT` | `/api/tenants/:id` | Authenticated |
352
+ | `DELETE` | `/api/tenants/:id` | Authenticated |
353
+ | `GET` | `/api/tenants/:id/users` | Authenticated |
354
+
355
+ ### Test
356
+
357
+ | Method | Path | Access |
358
+ | --- | --- | --- |
359
+ | `GET` | `/api/test` | Public |
360
+
361
+ The test controller is intended for development and should not be registered in production applications.
362
+
363
+ ### Users
364
+
365
+ #### User records and passwords
366
+
367
+ | Method | Path | Access |
368
+ | --- | --- | --- |
369
+ | `POST` | `/api/users` | Authenticated |
370
+ | `GET` | `/api/users` | Authenticated |
371
+ | `GET` | `/api/users/count` | Authenticated |
372
+ | `GET` | `/api/users/:id` | Authenticated |
373
+ | `PUT` | `/api/users/:id` | Authenticated |
374
+ | `DELETE` | `/api/users/:id` | Authenticated |
375
+ | `POST` | `/api/users/:id/password` | Authenticated |
376
+
377
+ #### User groups
378
+
379
+ | Method | Path | Access |
380
+ | --- | --- | --- |
381
+ | `POST` | `/api/users/:id/groups` | Authenticated |
382
+ | `GET` | `/api/users/:id/groups` | Authenticated |
383
+ | `PUT` | `/api/users/:id/groups` | Authenticated |
384
+ | `DELETE` | `/api/users/:id/groups` | Authenticated |
385
+
386
+ #### User tenants
387
+
388
+ | Method | Path | Access |
389
+ | --- | --- | --- |
390
+ | `POST` | `/api/users/:id/tenants` | Authenticated |
391
+ | `GET` | `/api/users/:id/tenants` | Authenticated |
392
+ | `PUT` | `/api/users/:id/tenants` | Authenticated |
393
+ | `DELETE` | `/api/users/:id/tenants` | Authenticated |
394
+
395
+ #### User API keys
396
+
397
+ | Method | Path | Access |
398
+ | --- | --- | --- |
399
+ | `POST` | `/api/users/keys` | Authenticated |
400
+ | `GET` | `/api/users/keys` | Authenticated |
401
+ | `GET` | `/api/users/keys/:id` | Authenticated |
402
+ | `POST` | `/api/users/keys/:id/revoke` | Authenticated |
403
+ | `DELETE` | `/api/users/keys/:id` | Authenticated |
404
+ | `POST` | `/api/users/:id/keys` | Authenticated |
405
+ | `GET` | `/api/users/:id/keys` | Authenticated |
406
+ | `GET` | `/api/users/:user/keys/:id` | Authenticated |
407
+ | `POST` | `/api/users/:id/keys/:key/revoke` | Authenticated |
408
+ | `DELETE` | `/api/users/:id/keys/:key` | Authenticated |
409
+
410
+ ## Initial setup
411
+
412
+ Initial users and authorization records are created through `SetupService`, not through an HTTP endpoint. Run it from a one-shot script after preparing the application and before starting the server.
413
+
414
+ ```ts
415
+ import { Application, SetupService } from "@andrewcaires/api";
416
+ import { controllers } from "./controllers";
417
+
418
+ const password = process.env.SETUP_ADMIN_PASSWORD;
419
+
420
+ if (!password) {
421
+
422
+ throw new Error("SETUP_ADMIN_PASSWORD is required");
423
+ }
424
+
425
+ const app = new Application(controllers);
426
+
427
+ await app.prepare();
428
+
429
+ await new SetupService().setup({
430
+ name: "Admin",
431
+ email: "admin@example.com",
432
+ username: "admin",
433
+ password,
434
+ });
435
+ ```
436
+
437
+ The setup is transactional, refuses to run when any user already exists, and requires an administrator password with at least 12 characters. Inject `SETUP_ADMIN_PASSWORD` only for the one-shot process through a secret manager or protected environment; do not commit it to an `.env` file.
438
+
83
439
  ### Links
84
440
 
85
441
  * [Docs](https://www.npmjs.com/package/@andrewcaires/api)