sashiko 0.8.0 → 0.10.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 (101) hide show
  1. package/README.md +262 -118
  2. package/dist/core/app.d.ts +35 -0
  3. package/dist/core/app.d.ts.map +1 -1
  4. package/dist/core/app.js +67 -8
  5. package/dist/core/app.js.map +1 -1
  6. package/dist/core/body-parser.d.ts +2 -1
  7. package/dist/core/body-parser.d.ts.map +1 -1
  8. package/dist/core/body-parser.js +7 -3
  9. package/dist/core/body-parser.js.map +1 -1
  10. package/dist/core/compiler.d.ts.map +1 -1
  11. package/dist/core/compiler.js +9 -5
  12. package/dist/core/compiler.js.map +1 -1
  13. package/dist/core/context.d.ts.map +1 -1
  14. package/dist/core/health.d.ts +26 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +62 -0
  17. package/dist/core/health.js.map +1 -0
  18. package/dist/core/index.d.ts +8 -6
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/index.js +5 -5
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/core/infer.d.ts +1 -3
  23. package/dist/core/infer.d.ts.map +1 -1
  24. package/dist/core/inject.d.ts.map +1 -1
  25. package/dist/core/inject.js.map +1 -1
  26. package/dist/core/logger.d.ts +17 -1
  27. package/dist/core/logger.d.ts.map +1 -1
  28. package/dist/core/logger.js +71 -3
  29. package/dist/core/logger.js.map +1 -1
  30. package/dist/core/metadata.js.map +1 -1
  31. package/dist/core/mounted-group.d.ts +8 -0
  32. package/dist/core/mounted-group.d.ts.map +1 -1
  33. package/dist/core/mounted-group.js +58 -18
  34. package/dist/core/mounted-group.js.map +1 -1
  35. package/dist/core/request-validation.d.ts.map +1 -1
  36. package/dist/core/request-validation.js +4 -1
  37. package/dist/core/request-validation.js.map +1 -1
  38. package/dist/core/response.d.ts.map +1 -1
  39. package/dist/core/response.js +4 -4
  40. package/dist/core/response.js.map +1 -1
  41. package/dist/core/route-table.d.ts.map +1 -1
  42. package/dist/core/route-table.js +1 -2
  43. package/dist/core/route-table.js.map +1 -1
  44. package/dist/core/shutdown.d.ts +5 -0
  45. package/dist/core/shutdown.d.ts.map +1 -1
  46. package/dist/core/shutdown.js.map +1 -1
  47. package/dist/decorators/http.d.ts.map +1 -1
  48. package/dist/decorators/http.js +2 -1
  49. package/dist/decorators/http.js.map +1 -1
  50. package/dist/decorators/log.d.ts.map +1 -1
  51. package/dist/decorators/log.js +1 -1
  52. package/dist/decorators/log.js.map +1 -1
  53. package/dist/decorators/middlewares.d.ts.map +1 -1
  54. package/dist/decorators/middlewares.js +1 -1
  55. package/dist/decorators/middlewares.js.map +1 -1
  56. package/dist/decorators/router.d.ts.map +1 -1
  57. package/dist/decorators/router.js +2 -1
  58. package/dist/decorators/router.js.map +1 -1
  59. package/dist/decorators/schema.d.ts.map +1 -1
  60. package/dist/decorators/schema.js +2 -1
  61. package/dist/decorators/schema.js.map +1 -1
  62. package/dist/middlewares/auth.d.ts +41 -0
  63. package/dist/middlewares/auth.d.ts.map +1 -0
  64. package/dist/middlewares/auth.js +74 -0
  65. package/dist/middlewares/auth.js.map +1 -0
  66. package/dist/middlewares/cors.d.ts.map +1 -1
  67. package/dist/middlewares/cors.js +5 -1
  68. package/dist/middlewares/cors.js.map +1 -1
  69. package/dist/middlewares/index.d.ts +1 -0
  70. package/dist/middlewares/index.d.ts.map +1 -1
  71. package/dist/middlewares/index.js +1 -0
  72. package/dist/middlewares/index.js.map +1 -1
  73. package/dist/openapi/generate.d.ts.map +1 -1
  74. package/dist/openapi/generate.js +5 -10
  75. package/dist/openapi/generate.js.map +1 -1
  76. package/dist/schema/validation.d.ts.map +1 -1
  77. package/dist/schema/validation.js +2 -0
  78. package/dist/schema/validation.js.map +1 -1
  79. package/dist/server/tls.d.ts +48 -0
  80. package/dist/server/tls.d.ts.map +1 -0
  81. package/dist/server/tls.js +60 -0
  82. package/dist/server/tls.js.map +1 -0
  83. package/dist/utils/content-type.d.ts.map +1 -1
  84. package/dist/utils/content-type.js +5 -2
  85. package/dist/utils/content-type.js.map +1 -1
  86. package/dist/utils/cookie.d.ts.map +1 -1
  87. package/dist/utils/cookie.js +1 -0
  88. package/dist/utils/cookie.js.map +1 -1
  89. package/dist/utils/origin-match.d.ts.map +1 -1
  90. package/dist/utils/origin-match.js +1 -2
  91. package/dist/utils/origin-match.js.map +1 -1
  92. package/dist/utils/path.d.ts.map +1 -1
  93. package/dist/utils/path.js.map +1 -1
  94. package/dist/utils/request-target.d.ts.map +1 -1
  95. package/dist/utils/request-target.js +4 -1
  96. package/dist/utils/request-target.js.map +1 -1
  97. package/dist/utils/safe-json.d.ts +19 -0
  98. package/dist/utils/safe-json.d.ts.map +1 -0
  99. package/dist/utils/safe-json.js +52 -0
  100. package/dist/utils/safe-json.js.map +1 -0
  101. package/package.json +5 -1
package/README.md CHANGED
@@ -42,6 +42,7 @@ await app.listen({ port: 3000 });
42
42
  - [Segurança](#segurança)
43
43
  - [Logs e request id](#logs-e-request-id)
44
44
  - [Ciclo de vida e testes](#ciclo-de-vida-e-testes)
45
+ - [Deploy em produção](#deploy-em-produção)
45
46
  - [Referência: `createApp(options)`](#referência-createappoptions)
46
47
  - [Princípios](#princípios)
47
48
 
@@ -54,7 +55,7 @@ await app.listen({ port: 3000 });
54
55
  ```bash
55
56
  pnpm add sashiko
56
57
  ```
57
- *(O TypeBox já vem incluído e re-exportado como `Type`, `Static` e `TSchema`)*
58
+ _(O TypeBox já vem incluído e re-exportado como `Type`, `Static` e `TSchema`)_
58
59
 
59
60
  `tsconfig.json` mínimo:
60
61
 
@@ -77,12 +78,16 @@ As rotas ficam em classes marcadas com `@Router`, a "permissão" para o app usar
77
78
  ```ts
78
79
  @Router('/users')
79
80
  class UserRoutes {
80
- @Get('/') list() {}
81
- @Get('/:id') get(ctx: Context) { return { id: ctx.params.id }; }
82
- @Post('/') create() {}
83
- @Put('/:id') replace() {}
84
- @Patch('/:id') update() {}
85
- @Delete('/:id') remove(ctx: Context) { ctx.status(204); }
81
+ @Get('/') list() {}
82
+ @Get('/:id') get(ctx: Context) {
83
+ return { id: ctx.params.id };
84
+ }
85
+ @Post('/') create() {}
86
+ @Put('/:id') replace() {}
87
+ @Patch('/:id') update() {}
88
+ @Delete('/:id') remove(ctx: Context) {
89
+ ctx.status(204);
90
+ }
86
91
  }
87
92
  ```
88
93
 
@@ -100,21 +105,22 @@ const userRoutes = new UserRoutes(repo);
100
105
 
101
106
  app
102
107
  .mount(HealthRoutes)
103
- .mount(userRoutes) // como declarado
104
- .mount(userRoutes, { prefix: '/v2/users' }) // a mesma instância em outro prefixo
108
+ .mount(userRoutes) // como declarado
109
+ .mount(userRoutes, { prefix: '/v2/users' }) // a mesma instância em outro prefixo
105
110
  .mount(userRoutes, { prefix: '/public/users', only: (r) => [r.list, r.get] }) // exposição parcial
106
111
  .mount(new AdminRoutes(repo), { middlewares: [authenticate], guards: [isAdmin] }); // regras para o grupo
107
112
  ```
108
113
 
109
- | Opção | Efeito |
110
- |---|---|
111
- | `prefix` | Substitui o prefixo do `@Router` |
114
+ | Opção | Efeito |
115
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `prefix` | Substitui o prefixo do `@Router` |
112
117
  | `only` / `except` | Registra só os métodos selecionados, ou todos menos esses: `(r) => [r.list, r.get]`. O seletor é checado pelo TypeScript; usar os dois juntos gera erro |
113
- | `auto` | `false` registra só as [rotas manuais](#mapeamento-manual). Padrão: `true` |
114
- | `middlewares` | Valem para todas as rotas do grupo; rodam antes dos `@Middlewares` da classe |
115
- | `guards` | Valem para todas as rotas do grupo; rodam antes dos `@Guards` da classe |
118
+ | `auto` | `false` registra só as [rotas manuais](#mapeamento-manual). Padrão: `true` |
119
+ | `middlewares` | Valem para todas as rotas do grupo; rodam antes dos `@Middlewares` da classe |
120
+ | `guards` | Valem para todas as rotas do grupo; rodam antes dos `@Guards` da classe |
116
121
 
117
122
  Erros de registro aparecem na inicialização, com mensagem clara:
123
+
118
124
  - classe sem `@Router`;
119
125
  - seletor em `only`/`except` que não aponta para um método de rota;
120
126
  - a mesma classe registrada duas vezes com o mesmo prefixo.
@@ -126,9 +132,7 @@ Erros de registro aparecem na inicialização, com mensagem clara:
126
132
  Para aplicar prefixo, middlewares e guards a **várias** classes de uma vez:
127
133
 
128
134
  ```ts
129
- app.group({ prefix: '/v2', middlewares: [authenticate], guards: [isAdmin] }, (g) =>
130
- g.mount(UserRoutes).mount(new OrderRoutes(db)),
131
- );
135
+ app.group({ prefix: '/v2', middlewares: [authenticate], guards: [isAdmin] }, (g) => g.mount(UserRoutes).mount(new OrderRoutes(db)));
132
136
  // GET /v2/users, GET /v2/orders…
133
137
  ```
134
138
 
@@ -142,9 +146,7 @@ app.group({ prefix: '/v2', middlewares: [authenticate], guards: [isAdmin] }, (g)
142
146
 
143
147
  ```ts
144
148
  export function audit(app: SashikoApp, opts: { store: AuditStore }) {
145
- return app
146
- .group({ prefix: '/audit', guards: [isAdmin] }, (g) => g.mount(new AuditRoutes(opts.store)))
147
- .onClose(() => opts.store.flush());
149
+ return app.group({ prefix: '/audit', guards: [isAdmin] }, (g) => g.mount(new AuditRoutes(opts.store))).onClose(() => opts.store.flush());
148
150
  }
149
151
 
150
152
  audit(app, { store });
@@ -157,16 +159,16 @@ Depois do `mount`, os verbos `.get()`, `.post()`, `.put()`, `.patch()` e `.delet
157
159
  ```ts
158
160
  @Router('/users')
159
161
  class UserRoutes {
160
- @Get('/:id', byId) getUser(ctx: Context<typeof byId>) {}
161
- @Schema(csv) exportCsv(ctx: Context<typeof csv>) {} // sem verbo: só por mapeamento manual
162
- private findOr404() {} // sem decorator: nunca pode ser exposto
162
+ @Get('/:id', byId) getUser(ctx: Context<typeof byId>) {}
163
+ @Schema(csv) exportCsv(ctx: Context<typeof csv>) {} // sem verbo: só por mapeamento manual
164
+ private findOr404() {} // sem decorator: nunca pode ser exposto
163
165
  }
164
166
 
165
167
  app
166
168
  .mount(new UserRoutes(repo))
167
- .get('/legacy/:id', (r) => r.getUser) // GET /users/legacy/:id
169
+ .get('/legacy/:id', (r) => r.getUser) // GET /users/legacy/:id
168
170
  .get('/export.csv', (r) => r.exportCsv, { middlewares: [cache()], guards: [isAuditor] })
169
- .mount(HealthRoutes); // o encadeamento continua
171
+ .mount(HealthRoutes); // o encadeamento continua
170
172
  ```
171
173
 
172
174
  - **Seletor:** funciona com instância (inclusive inline, `mount(new X(repo))`) e com a classe. "Ir para a definição" e renomear no editor funcionam.
@@ -207,11 +209,13 @@ list(ctx: Context<typeof list>) {
207
209
  ```
208
210
 
209
211
  **Path e `params` conferidos**
212
+
210
213
  - **Em compilação:** com schema de `params` no verbo, todo parâmetro do path precisa estar no schema. `@Get('/:userId', { params: Type.Object({ id: Type.String() }) })` é erro de tipo (`Missing in params schema: "userId"`).
211
214
  - **No registro:** o `app.mount()` confere path x schema nos dois sentidos, já com o prefixo do `@Router`/`mount` (que pode ter parâmetros, como `'/orgs/:org'`), também para `@Schema` separado e rotas manuais. Também recusa path malformado: `/:`, nome repetido, regex sem fechamento, `*` fora do fim.
212
215
  - **Sem schema:** `Context<'/:org/users/:id'>` tipa `ctx.params` pelo path (`{ org: string; id: string }`). O utilitário `PathParams<'/:a/:b'>` dá `'a' | 'b'`.
213
216
 
214
217
  **Validação**
218
+
215
219
  - **Tipagem:** com `Context<typeof schemas>`, `ctx.params`, `ctx.query` e `ctx.body` são tipados a partir do schema. Sem o genérico, `Context` usa tipos genéricos.
216
220
  - **Coerção:** params, query e headers chegam como texto e são convertidos para o tipo declarado. O **body não sofre coerção**: `"30"` não vira `30`.
217
221
  - **Defaults:** valores `default` são aplicados.
@@ -228,6 +232,7 @@ list(ctx: Context<typeof list>) {
228
232
  - **Campos extras:** use `additionalProperties: false` nos bodies para rejeitar campos não previstos. É a proteção contra mass assignment.
229
233
 
230
234
  **Resposta**
235
+
231
236
  - **Filtro:** o schema de `response` serializa a saída com `fast-json-stringify`, e **campos não declarados não saem**, por exemplo `password`.
232
237
  - **Status padrão:** se só existe um 2xx declarado (como `201`), ele é o status padrão; não é preciso chamar `ctx.status(201)`.
233
238
  - **Status sem schema:** se a rota declara schemas de resposta e o handler devolve um status sem schema, a resposta é **500**, com o motivo no log, e não o objeto sem filtro.
@@ -239,13 +244,13 @@ list(ctx: Context<typeof list>) {
239
244
 
240
245
  O valor retornado pelo handler vira a resposta. Sem schema de resposta, o formato vem do tipo do valor:
241
246
 
242
- | Retorno | Resposta |
243
- |---|---|
244
- | objeto, array, número, booleano, `null` | `application/json` |
245
- | `string` | `text/plain; charset=utf-8`, sem aspas |
246
- | `Buffer` / `Uint8Array` | `application/octet-stream` |
247
- | `Readable` (stream) | enviado em stream |
248
- | `undefined` | sem corpo |
247
+ | Retorno | Resposta |
248
+ | --------------------------------------- | -------------------------------------- |
249
+ | objeto, array, número, booleano, `null` | `application/json` |
250
+ | `string` | `text/plain; charset=utf-8`, sem aspas |
251
+ | `Buffer` / `Uint8Array` | `application/octet-stream` |
252
+ | `Readable` (stream) | enviado em stream |
253
+ | `undefined` | sem corpo |
249
254
 
250
255
  Um `Content-Type` definido no handler (`ctx.header('Content-Type', ...)`) sempre prevalece.
251
256
 
@@ -295,6 +300,7 @@ run(ctx: Context<typeof job>) {
295
300
  **O `status()` tipa o corpo.** Com schemas de resposta, `ctx.status()` só aceita os status declarados, e o `json()`/`send()` seguinte exige o corpo daquele status. `ctx.status(202).json({ result })` e `ctx.status(201)` são erros de compilação no exemplo acima.
296
301
 
297
302
  Estilo recomendado:
303
+
298
304
  - `return` para o caso comum;
299
305
  - `ctx.status(x).json(body)` quando o status é decidido em runtime;
300
306
  - `throw` para erros.
@@ -303,23 +309,23 @@ Os schemas de resposta valem para o que o **handler** envia. Middlewares e guard
303
309
 
304
310
  ### Helpers do `ctx`
305
311
 
306
- | Método | Uso |
307
- |---|---|
308
- | `ctx.status(code)` | Define o status |
309
- | `ctx.header(name, value)` | Define um header da resposta |
310
- | `ctx.get(name)` | Lê um header da requisição |
311
- | `ctx.json(data)` | Envia JSON pelo pipeline (schema, filtro, status padrão) |
312
- | `ctx.text(str)` | `text/plain; charset=utf-8`, pelo pipeline |
313
- | `ctx.send(body, contentType?)` | Qualquer corpo pelo pipeline: string, bytes, stream ou objeto (negociado pelo `Accept`) |
314
- | `ctx.redirect(url, status?)` | 302 por padrão; aceita 301, 303, 307, 308 |
315
- | `ctx.cookies` | Cookies da requisição (objeto sem protótipo) |
312
+ | Método | Uso |
313
+ | ----------------------------------- | ---------------------------------------------------------------------------------------- |
314
+ | `ctx.status(code)` | Define o status |
315
+ | `ctx.header(name, value)` | Define um header da resposta |
316
+ | `ctx.get(name)` | Lê um header da requisição |
317
+ | `ctx.json(data)` | Envia JSON pelo pipeline (schema, filtro, status padrão) |
318
+ | `ctx.text(str)` | `text/plain; charset=utf-8`, pelo pipeline |
319
+ | `ctx.send(body, contentType?)` | Qualquer corpo pelo pipeline: string, bytes, stream ou objeto (negociado pelo `Accept`) |
320
+ | `ctx.redirect(url, status?)` | 302 por padrão; aceita 301, 303, 307, 308 |
321
+ | `ctx.cookies` | Cookies da requisição (objeto sem protótipo) |
316
322
  | `ctx.setCookie(name, value, opts?)` | Adiciona um `Set-Cookie`, com `HttpOnly`, `Secure`, `SameSite=Lax` e `Path=/` por padrão |
317
- | `ctx.clearCookie(name, opts?)` | Remove um cookie (use o mesmo `path` e `domain` da criação) |
318
- | `ctx.throw(status, message)` | Lança um `HttpError` |
323
+ | `ctx.clearCookie(name, opts?)` | Remove um cookie (use o mesmo `path` e `domain` da criação) |
324
+ | `ctx.throw(status, message)` | Lança um `HttpError` |
319
325
 
320
326
  Outros campos: `ctx.params`, `ctx.query`, `ctx.body`, `ctx.state`, `ctx.id`, `ctx.log`, `ctx.sentBody`, `ctx.sentType`, `ctx.ip`, `ctx.host`, `ctx.hostname`, `ctx.protocol`, `ctx.method`, `ctx.path`, `ctx.url`, `ctx.req` e `ctx.res`.
321
327
 
322
- > ⚠️ `ctx.send` não faz escape de HTML; escape o conteúdo dinâmico antes. `ctx.redirect` com uma URL vinda do usuário abre um *open redirect*; valide o destino.
328
+ > ⚠️ `ctx.send` não faz escape de HTML; escape o conteúdo dinâmico antes. `ctx.redirect` com uma URL vinda do usuário abre um _open redirect_; valide o destino.
323
329
 
324
330
  ## Middlewares e guards
325
331
 
@@ -337,7 +343,9 @@ Por classe ou por método, use `@Middlewares` e `@Guards`; para um grupo inteiro
337
343
 
338
344
  ```ts
339
345
  declare module 'sashiko' {
340
- interface ContextState { user?: { id: string; role: 'admin' | 'user' } } // ctx.state tipado
346
+ interface ContextState {
347
+ user?: { id: string; role: 'admin' | 'user' };
348
+ } // ctx.state tipado
341
349
  }
342
350
 
343
351
  const authenticate: Middleware = async (ctx, next) => {
@@ -373,9 +381,13 @@ Para o que acontece **antes** do handler (autenticação, rate limit, preparar o
373
381
 
374
382
  ```ts
375
383
  app
376
- .onSend((ctx, payload) => { ctx.header('ETag', etag(payload.body)); }) // antes de escrever (síncrono)
384
+ .onSend((ctx, payload) => {
385
+ ctx.header('ETag', etag(payload.body));
386
+ }) // antes de escrever (síncrono)
377
387
  .onResponse((ctx, { status, ms }) => metrics.observe(ctx.path, status, ms)) // depois do envio
378
- .onClose(async () => { await db.close(); }); // em qualquer close()
388
+ .onClose(async () => {
389
+ await db.close();
390
+ }); // em qualquer close()
379
391
  ```
380
392
 
381
393
  - **`onSend`:** roda imediatamente antes de escrever uma resposta com corpo (exceto streams), inclusive erros e 404. É **síncrono**, como a escrita do pipeline. Pode alterar headers ou devolver `{ body, contentType }` novos. Se lançar, o erro é logado e a resposta original é enviada.
@@ -385,23 +397,25 @@ app
385
397
  **Cache com middleware.** Depois do `await next()`, `ctx.sentBody` e `ctx.sentType` contêm o corpo exatamente como foi enviado, já filtrado pelo schema:
386
398
 
387
399
  ```ts
388
- const cache = (ttlMs: number): Middleware => async (ctx, next) => {
389
- const hit = store.get(ctx.url.href);
390
- if (hit) return void ctx.header('X-Cache', 'HIT').send(hit.body, hit.type);
391
- ctx.header('X-Cache', 'MISS'); // headers antes do next(): depois dele a resposta já foi enviada
392
- await next();
393
- if (ctx.res.statusCode === 200 && ctx.sentBody) store.set(ctx.url.href, { body: ctx.sentBody, type: ctx.sentType! });
394
- };
400
+ const cache =
401
+ (ttlMs: number): Middleware =>
402
+ async (ctx, next) => {
403
+ const hit = store.get(ctx.url.href);
404
+ if (hit) return void ctx.header('X-Cache', 'HIT').send(hit.body, hit.type);
405
+ ctx.header('X-Cache', 'MISS'); // headers antes do next(): depois dele a resposta já foi enviada
406
+ await next();
407
+ if (ctx.res.statusCode === 200 && ctx.sentBody) store.set(ctx.url.href, { body: ctx.sentBody, type: ctx.sentType! });
408
+ };
395
409
  app.mount(products, { middlewares: [cache(60_000)] });
396
410
  ```
397
411
 
398
- | Fastify | Sashiko |
399
- |---|---|
412
+ | Fastify | Sashiko |
413
+ | ------------------------- | ----------------------------------------------------------------- |
400
414
  | `onRequest`, `preHandler` | middlewares (`app.use`, `mount({ middlewares })`, `@Middlewares`) |
401
- | `onError` | `app.onError` |
402
- | `onSend` | `app.onSend` (síncrono) |
403
- | `onResponse` | `app.onResponse` |
404
- | `onClose` | `app.onClose` |
415
+ | `onError` | `app.onError` |
416
+ | `onSend` | `app.onSend` (síncrono) |
417
+ | `onResponse` | `app.onResponse` |
418
+ | `onClose` | `app.onClose` |
405
419
 
406
420
  ## Erros
407
421
 
@@ -420,14 +434,14 @@ Todos os erros, inclusive 404, 405, validação e `Host` inválido, saem no form
420
434
  { "type": "about:blank", "title": "Not Found", "status": 404, "detail": "user not found", "code": "NOT_FOUND" }
421
435
  ```
422
436
 
423
- | Campo | Conteúdo |
424
- |---|---|
425
- | `type` | URI do tipo do problema (opção `type` do `HttpError`); `about:blank` por padrão |
426
- | `title` | Frase do status (`Not Found`) |
427
- | `status` | Status HTTP |
428
- | `detail` | A mensagem do erro; omitido quando igual ao título ou em 5xx não expostos |
429
- | `code` | Código estável para o cliente tratar: `VALIDATION_ERROR`, `INVALID_JSON`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `METHOD_NOT_ALLOWED`, `NOT_ACCEPTABLE`, `INVALID_HOST`, `FORBIDDEN`, `RATE_LIMITED`, `CSRF_ORIGIN`, `CSRF_TOKEN`… |
430
- | `details` | Dados extras: os erros de validação, `allowed` no 405, `available` no 406 |
437
+ | Campo | Conteúdo |
438
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
439
+ | `type` | URI do tipo do problema (opção `type` do `HttpError`); `about:blank` por padrão |
440
+ | `title` | Frase do status (`Not Found`) |
441
+ | `status` | Status HTTP |
442
+ | `detail` | A mensagem do erro; omitido quando igual ao título ou em 5xx não expostos |
443
+ | `code` | Código estável para o cliente tratar: `VALIDATION_ERROR`, `INVALID_JSON`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `METHOD_NOT_ALLOWED`, `NOT_ACCEPTABLE`, `INVALID_HOST`, `FORBIDDEN`, `RATE_LIMITED`, `CSRF_ORIGIN`, `CSRF_TOKEN`… |
444
+ | `details` | Dados extras: os erros de validação, `allowed` no 405, `available` no 406 |
431
445
 
432
446
  - **Erros 4xx:** a mensagem vai para o cliente.
433
447
  - **Erros 5xx e erros comuns** (`throw new Error(...)`): a resposta é sempre `{ "type": "about:blank", "title": "Internal Server Error", "status": 500 }`. O erro completo vai para o log, com `reqId`. Para mostrar uma mensagem de 5xx ao cliente, use `expose: true`.
@@ -442,8 +456,8 @@ createApp()
442
456
  .openapi({
443
457
  info: { title: 'Minha API', version: '1.0.0' },
444
458
  servers: [{ url: 'https://api.exemplo.dev' }],
445
- docs: true, // Swagger UI em /docs (desligado por padrão)
446
- guards: [isAdmin], // só para /openapi.json e /docs
459
+ docs: true, // Swagger UI em /docs (desligado por padrão)
460
+ guards: [isAdmin], // só para /openapi.json e /docs
447
461
  });
448
462
  ```
449
463
 
@@ -455,14 +469,14 @@ createApp()
455
469
  - **`app.openapiDocument()`:** devolve o objeto, para gravar em arquivo, gerar clientes ou testar no CI.
456
470
  - **O que entra no documento:**
457
471
 
458
- | Origem | Vira |
459
- |---|---|
460
- | Path (`/:id(^\\d+)`) | `/{id}` |
461
- | `params`, `query`, `headers` | `parameters`, com `required` |
462
- | `body` | `requestBody` em JSON |
463
- | `response` | uma resposta por status, com os `contentType` declarados |
464
- | Erros | resposta `default` com o schema `ProblemDetails`; com `formatError`, só a descrição |
465
- | Classe e método | `tags` e `operationId` (`UserRoutes.getUser`) |
472
+ | Origem | Vira |
473
+ | ---------------------------- | ----------------------------------------------------------------------------------- |
474
+ | Path (`/:id(^\\d+)`) | `/{id}` |
475
+ | `params`, `query`, `headers` | `parameters`, com `required` |
476
+ | `body` | `requestBody` em JSON |
477
+ | `response` | uma resposta por status, com os `contentType` declarados |
478
+ | Erros | resposta `default` com o schema `ProblemDetails`; com `formatError`, só a descrição |
479
+ | Classe e método | `tags` e `operationId` (`UserRoutes.getUser`) |
466
480
 
467
481
  - **Limites:** só schemas JSON Schema (TypeBox) entram como estão. Rotas sem schema de resposta aparecem com um `200` genérico. Em produção, proteja as rotas com `guards`/`middlewares` ou gere só o arquivo com `openapiDocument()`.
468
482
 
@@ -470,15 +484,17 @@ createApp()
470
484
 
471
485
  Ligado por padrão:
472
486
 
473
- | Proteção | Detalhe |
474
- |---|---|
487
+ | Proteção | Detalhe |
488
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
475
489
  | Headers de segurança | `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`, `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `Cross-Origin-Resource-Policy: same-origin`. HSTS é opt-in. Para servir páginas, use `securityHeaders: 'web'` (CSP `default-src 'self'`, `X-Frame-Options: SAMEORIGIN`). |
476
- | Limite de body | 1 MiB (`bodyLimit`). Recusado já pelo `Content-Length`, com a conexão fechada. |
477
- | Timeouts | `requestTimeout` 30s, `headersTimeout` 10s, `keepAliveTimeout` 5s (contra Slowloris). |
478
- | Query | Até 1000 pares (`queryMaxKeys`); `__proto__`, `constructor` e `prototype` são descartados. |
479
- | Erros 5xx | Sem detalhes para o cliente. |
480
- | Request id | Ids recebidos só são aceitos se forem seguros (contra log injection). |
481
- | Host e path | `Host` malformado → 400. O path vem do request-target com base fixa: `//evil.com/x` continua sendo esse path e não muda o host. `ctx.url` usa o host validado. |
490
+ | Limite de body | 1 MiB (`bodyLimit`). Recusado já pelo `Content-Length`, com a conexão fechada. |
491
+ | Timeouts | `requestTimeout` 30s, `headersTimeout` 10s, `keepAliveTimeout` 5s (contra Slowloris). |
492
+ | Query | Até 1000 pares (`queryMaxKeys`); `__proto__`, `constructor` e `prototype` são descartados. |
493
+ | Body JSON | `__proto__` e `constructor.prototype` em qualquer nível, inclusive com escapes `\u`, recebem 400 (`PROTO_POISONING`). Com `protoPoisoning: 'remove'`, as chaves são descartadas e a requisição segue. |
494
+ | Logs | `authorization`, `cookie`, `password`, `token`, `apiKey` e afins saem como `[REDACTED]` no logger padrão. |
495
+ | Erros 5xx | Sem detalhes para o cliente. |
496
+ | Request id | Ids recebidos só são aceitos se forem seguros (contra log injection). |
497
+ | Host e path | `Host` malformado → 400. O path vem do request-target com base fixa: `//evil.com/x` continua sendo esse path e não muda o host. `ctx.url` usa o host validado. |
482
498
 
483
499
  Opt-in:
484
500
 
@@ -490,22 +506,31 @@ createApp({
490
506
  securityHeaders: { strictTransportSecurity: true }, // só atrás de HTTPS
491
507
  })
492
508
  .use(cors({ origin: ['https://app.exemplo.com', 'https://*.exemplo.dev'], credentials: true }))
493
- .use(rateLimit({ max: 100, windowMs: 60_000 })); // 429 + Retry-After
509
+ .use(rateLimit({ max: 100, windowMs: 60_000 })); // 429 + Retry-After
494
510
  ```
495
511
 
496
512
  - **`cors()`:** a origem é obrigatória, sem default permissivo. Responde ao preflight com 204 e envia `Vary: Origin`. Combinar `origin: '*'` com `credentials: true` gera erro.
497
513
  - **Regras de origem e de host, seguras por padrão** (as mesmas para `cors()` e `allowedHosts`):
498
514
 
499
- | Regra | Casa com | Não casa com |
500
- |---|---|---|
515
+ | Regra | Casa com | Não casa com |
516
+ | --------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
501
517
  | `'https://app.exemplo.com'` | a mesma origem, depois de normalizar (maiúsculas, porta padrão, barra final) | `http://…`, outra porta, `app.exemplo.com.atacante.com` |
502
- | `'https://*.exemplo.dev'` | qualquer subdomínio | `exemplo.dev` em si, `exemplo.dev.atacante.com` |
503
- | RegExp | a string **inteira**, como se tivesse `^…$` | qualquer casamento parcial |
504
- | função | o que ela decidir, sob sua responsabilidade | — |
505
-
518
+ | `'https://*.exemplo.dev'` | qualquer subdomínio | `exemplo.dev` em si, `exemplo.dev.atacante.com` |
519
+ | RegExp | a string **inteira**, como se tivesse `^…$` | qualquer casamento parcial |
520
+ | função | o que ela decidir, sob sua responsabilidade | — |
506
521
  - **RegExp sem âncora não vira brecha:** `/app\.exemplo\.com/` não aceita `app.exemplo.com.atacante.com`. As flags `g`, `y` e `m` são descartadas, então o resultado não muda entre requisições.
507
522
  - **Prefira o curinga à RegExp:** uma regex mal escrita pode ser lenta com uma entrada maliciosa (ReDoS).
508
523
  - **Regras inválidas falham na inicialização,** por exemplo uma origem sem esquema (`'app.exemplo.com'`).
524
+ - **Autenticação: `bearerAuth()` e `basicAuth()`.** São guards prontos. A verificação é sua: JWT com a biblioteca que preferir, sessão no banco, lista de chaves. O usuário devolvido vai para `ctx.state.user`.
525
+
526
+ ```ts
527
+ const auth = bearerAuth({ verify: async (token) => (await sessions.find(token)) ?? false });
528
+ app.group({ guards: [auth] }, (g) => g.mount(UserRoutes));
529
+
530
+ const painel = basicAuth({ verify: (user, pass) => safeEqual(user, 'admin') && safeEqual(pass, process.env.ADMIN_PASS!) });
531
+ ```
532
+
533
+ Credencial ausente, malformada ou recusada recebe **o mesmo** 401, com `WWW-Authenticate` e sem dizer o motivo. Para comparar segredos, use `safeEqual()`, que tem tempo constante e também não revela o tamanho. Use o `basicAuth` só sobre HTTPS.
509
534
  - **`rateLimit()`:** guarda os contadores em memória por padrão. Para várias instâncias, implemente `RateLimitStore` (Redis etc.).
510
535
  - **Cookies:** `ctx.setCookie()` usa `HttpOnly`, `Secure`, `SameSite=Lax` e `Path=/` por padrão (os navegadores aceitam `Secure` em `http://localhost`). Nome, `path` e `domain` são validados contra injeção de atributos, e os prefixos `__Host-` e `__Secure-` têm as regras conferidas.
511
536
  - **`csrf()`: ative sempre que a autenticação usar cookies.** Métodos que alteram estado (todos menos GET, HEAD e OPTIONS) vindos de outra origem recebem 403. A checagem usa o `Sec-Fetch-Site` e, na falta dele, compara o `Origin` com a própria origem. Requisições sem esses headers (curl, servidor a servidor) passam, porque não há navegador para anexar os cookies da vítima.
@@ -515,6 +540,7 @@ createApp({
515
540
  app.use(csrf({ trustedOrigins: ['https://app.exemplo.com'] })); // front em outra origem
516
541
  app.use(csrf({ token: true })); // + double-submit token
517
542
  ```
543
+
518
544
  Com `token: true`, o middleware cria o cookie `__Host-csrf` (legível pelo JS, `SameSite=Strict`) e o expõe em `ctx.state.csrfToken`; o cliente o devolve no header `X-CSRF-Token`. Rotas GET nunca devem alterar estado.
519
545
  - **Atrás de proxy:** `ctx.ip`, `ctx.host` e `ctx.protocol` usam o socket e o `Host` por padrão. Com `trustProxy: 1` (o número de proxies à frente do app) ou `true` (toda a cadeia), passam a usar o `X-Forwarded-For`, o `-Host` e o `-Proto`. Só ligue isso atrás de um proxy que reescreve esses headers, porque o cliente pode forjá-los.
520
546
  - **`allowedHosts`:** com `allowedHosts: ['api.exemplo.com', '*.exemplo.dev']`, qualquer outro `Host` recebe 400. Protege contra host header injection e DNS rebinding. Um host sem porta aceita qualquer porta; com porta (`'api.exemplo.com:8443'`), exige a porta exata.
@@ -528,11 +554,17 @@ import pino from 'pino';
528
554
  createApp({ logger: pino(), logRequests: true });
529
555
  ```
530
556
 
531
- - **Logger:** compatível com o pino (`info(obj, msg)`, `child()`). O padrão, `consoleLogger({ level })`, usa o `console`. `logger: false` desliga tudo.
557
+ - **Logger:** compatível com o pino (`info(obj, msg)`, `child()`). O padrão, `consoleLogger({ level, format, redact })`, usa o `console`: `format: 'json'` gera uma linha JSON por evento, para coletores de log. `logger: false` desliga tudo.
532
558
  - **Request id:** cada requisição recebe `ctx.id`, também devolvido no header `X-Request-Id`. Um id recebido nesse header é reaproveitado se for seguro; isso é configurável em `requestId`.
533
559
  - **`ctx.log`:** já inclui o `reqId`: `ctx.log.info({ userId }, 'user created')`.
534
560
  - **`logRequests: true`:** registra método, path, status e duração de cada requisição.
535
- - **Dados sensíveis:** headers e body nunca são logados automaticamente.
561
+ - **Dados sensíveis:** headers e body nunca são logados automaticamente. O que você logar passa pelo `redact` do logger padrão. As chaves `authorization`, `cookie`, `set-cookie`, `x-api-key`, `password`, `secret`, `token`, `access_token`, `refresh_token` e afins, em qualquer nível, saem como `[REDACTED]`. A comparação ignora maiúsculas, `-` e `_`.
562
+
563
+ ```ts
564
+ createApp({ logger: consoleLogger({ redact: ['cpf', 'cardNumber'] }) }); // soma às chaves padrão; false desliga
565
+ ```
566
+
567
+ Com o pino, configure o [`redact`](https://getpino.io/#/docs/redaction) dele.
536
568
 
537
569
  ## Ciclo de vida e testes
538
570
 
@@ -541,17 +573,69 @@ const app = createApp().mount(UserRoutes);
541
573
 
542
574
  await app.listen({ port: 3000, host: '0.0.0.0' }); // resolve quando está ouvindo; SIGINT/SIGTERM já tratados
543
575
 
544
- // Servidor próprio (HTTPS, http2 em modo compat…): aqui o ciclo de vida é seu
576
+ await app.listen({ port: 443, host: '0.0.0.0', tls: { key, cert } }); // HTTPS, com o mesmo ciclo de vida
577
+
578
+ // Servidor próprio (http2 em modo compat, outro transporte…): aqui o ciclo de vida é seu
545
579
  https.createServer(tlsOptions, app.callback()).listen(443);
546
580
  ```
547
581
 
582
+ ### HTTPS
583
+
584
+ ```ts
585
+ import { readFileSync } from 'node:fs';
586
+
587
+ await app.listen({
588
+ port: 443,
589
+ host: '0.0.0.0',
590
+ tls: {
591
+ key: readFileSync('/etc/ssl/privkey.pem'),
592
+ cert: readFileSync('/etc/ssl/fullchain.pem'),
593
+ // pfx + passphrase também funcionam; minVersion: 'TLSv1.3' para exigir TLS 1.3
594
+ },
595
+ });
596
+ ```
597
+
598
+ - **Mesmo ciclo de vida do HTTP:** timeouts contra Slowloris, encerramento gracioso nos sinais, `close()` e `onClose`. `ctx.protocol` vira `https`, e os cookies `Secure` funcionam.
599
+ - **TLS 1.2 é o mínimo.** SSLv3, TLS 1.0 e 1.1 não são aceitos, nem por configuração.
600
+ - **Certificados inválidos falham no `listen()`,** e não no primeiro cliente.
601
+ - **Vários domínios (SNI):** cada domínio recebe o seu certificado no handshake.
602
+
603
+ ```ts
604
+ tls: {
605
+ key, cert, // principal: domínios sem entrada própria
606
+ sni: {
607
+ 'api.exemplo.com': { key: apiKey, cert: apiCert },
608
+ '*.exemplo.dev': { key: devKey, cert: devCert }, // um nível: a.exemplo.dev, não a.b.exemplo.dev
609
+ },
610
+ }
611
+ ```
612
+
613
+ Sem certificado principal, um domínio fora da lista não completa o handshake. Para recusar o domínio também na camada HTTP, combine com `allowedHosts`.
614
+ - **Renovação sem reiniciar:** `app.reloadTLS({ key, cert, sni })` troca os certificados. As conexões novas usam os novos, e as abertas continuam com os antigos. Um certificado inválido lança erro e mantém os atuais. É o ponto de integração para um cliente ACME externo, como Let's Encrypt via certbot:
615
+
616
+ ```ts
617
+ watch('/etc/ssl/fullchain.pem', () => app.reloadTLS({ key: readFileSync(keyPath), cert: readFileSync(certPath) }));
618
+ ```
619
+
620
+ **TLS no app ou no proxy?** Na maioria dos deploys, o TLS termina no proxy ou no load balancer (Caddy, Nginx, Traefik). Nesse caso, use `app.listen()` em HTTP na rede interna, com `trustProxy: 1` e `allowedHosts`. Use `listen({ tls })` quando não houver proxy à frente ou quando o TLS precisar ir até o processo.
621
+
622
+ **Fora do core, por decisão:** emissão e renovação automática de certificados (ACME) e gerenciamento de DNS. São responsabilidades de ferramentas próprias, integradas pelo `reloadTLS()`.
623
+
624
+ ### Planos futuros
625
+
626
+ - **HTTP/2** (com fallback para HTTP/1.1 via ALPN). Ainda não é suportado pelo `listen()`. O modo de compatibilidade do Node muda partes do ciclo de vida: o shutdown usa GOAWAY, e não `Connection: close`; não existem `requestTimeout` nem `headersTimeout`; os limites de streams precisam de padrão seguro. Por isso, entra numa versão própria. Até lá, o proxy à frente pode falar HTTP/2 com o cliente e HTTP/1.1 com o app.
627
+ - **Virtual hosts:** `mount(X, { host })` e `group({ host })`, para servir domínios diferentes no mesmo app.
628
+ - **mTLS:** certificado do cliente (`requestCert`) e `ctx.clientCertificate`.
629
+
548
630
  **Encerramento gracioso, sem código.** Com `listen()`, um SIGINT ou SIGTERM faz o app parar de aceitar conexões, esperar as requisições em andamento (com prazo) e sair com código 0:
549
631
 
550
632
  ```ts
551
633
  createApp({
552
634
  shutdown: {
553
- timeoutMs: 10_000, // prazo para as requisições em andamento (padrão)
554
- onShutdown: async () => { await db.close(); }, // depois que o servidor fecha
635
+ timeoutMs: 10_000, // prazo para as requisições em andamento (padrão)
636
+ onShutdown: async () => {
637
+ await db.close();
638
+ }, // depois que o servidor fecha
555
639
  },
556
640
  });
557
641
  ```
@@ -560,6 +644,28 @@ createApp({
560
644
  - **Vários apps no mesmo processo:** compartilham um único listener por sinal e fecham todos juntos.
561
645
  - **Limpeza:** os listeners são removidos no `close()`, então testes com vários `listen(0)` não acumulam listeners. O `inject()` e o `callback()` não registram nada.
562
646
  - **Opções:** `signals` muda os sinais tratados, `exit: false` fecha sem chamar `process.exit`, e `shutdown: false` desliga tudo (por exemplo, se outra ferramenta controla o processo).
647
+ - **Drain para load balancers:** com `shutdown: { drainDelayMs: 5000 }`, o app espera esse tempo antes de parar de aceitar conexões, já com o `/readyz` em 503, para o balanceador tirar a instância de rotação sem cortar requisições.
648
+
649
+ ### Healthchecks
650
+
651
+ ```ts
652
+ app.health({
653
+ checks: {
654
+ db: () => db.query('select 1'), // lançar ou devolver false = falha
655
+ cache: () => redis.ping(),
656
+ },
657
+ timeoutMs: 2000, // por check (padrão)
658
+ });
659
+ ```
660
+
661
+ | Rota | Responde | Uso |
662
+ | ------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------- |
663
+ | `GET /livez` | 200 `{ "status": "ok" }` enquanto o processo estiver de pé | liveness: reiniciar o processo travado |
664
+ | `GET /readyz` | 200 com o resultado de cada check; 503 se algum falhar ou estourar o tempo, e durante o encerramento | readiness: tirar a instância do balanceamento |
665
+
666
+ - **Os checks rodam em paralelo.** O motivo da falha vai só para o log, nunca para a resposta, porque pode conter host, usuário ou URL do banco.
667
+ - **As rotas são servidas antes dos middlewares globais,** já que sondas não têm credenciais nem devem consumir rate limit. Para restringir, use `guards`/`middlewares` no próprio `health()`.
668
+ - **Opções:** `live: '/health'` muda o path, e `ready: false` desliga a rota.
563
669
 
564
670
  **Testes sem rede.** `app.inject()` executa a requisição em memória:
565
671
 
@@ -569,25 +675,60 @@ assert.equal(res.status, 201);
569
675
  assert.equal(res.json<{ name: string }>().name, 'Ana');
570
676
  ```
571
677
 
678
+ ## Deploy em produção
679
+
680
+ ```ts
681
+ const app = createApp({
682
+ logger: consoleLogger({ format: 'json' }), // uma linha JSON por evento, com redact
683
+ trustProxy: 1, // atrás de 1 proxy/load balancer
684
+ allowedHosts: ['api.exemplo.com'],
685
+ securityHeaders: { strictTransportSecurity: true }, // HSTS: só com HTTPS na frente
686
+ shutdown: { drainDelayMs: 5_000, timeoutMs: 10_000 },
687
+ })
688
+ .use(rateLimit({ max: 100, windowMs: 60_000 }))
689
+ .mount(UserRoutes)
690
+ .health({ checks: { db: () => db.ping() } });
691
+
692
+ await app.listen({ port: Number(process.env.PORT ?? 3000), host: '0.0.0.0' });
693
+ ```
694
+
695
+ Checklist:
696
+
697
+ | Item | Como |
698
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
699
+ | Escutar fora do container | `host: '0.0.0.0'` (o padrão é `127.0.0.1`), porta por `PORT` |
700
+ | TLS | No proxy ou load balancer (recomendado), ou `listen({ tls })` |
701
+ | Proxy à frente | `trustProxy` com o número **exato** de proxies; nunca `true` se o app for acessível direto |
702
+ | Domínios | `allowedHosts` com os domínios servidos |
703
+ | HSTS | `securityHeaders: { strictTransportSecurity: true }`, só quando todo acesso for HTTPS |
704
+ | Logs | `consoleLogger({ format: 'json' })` ou pino; campos sensíveis já mascarados |
705
+ | Healthchecks | `/livez` (liveness) e `/readyz` (readiness) com `app.health()` |
706
+ | Encerramento | SIGTERM já tratado; `drainDelayMs` maior que o intervalo da sonda de readiness |
707
+ | Rate limit com várias instâncias | `RateLimitStore` compartilhado (Redis), porque o padrão é em memória por processo |
708
+ | Autenticação por cookie | `csrf()` ativo |
709
+ | Documentação da API | `openapi({ guards })`, ou gerar só o arquivo com `openapiDocument()` |
710
+ | Processo | Rode `node dist/server.js` direto (não via `npm start`), para o SIGTERM chegar ao Node; em container, `--init` ou `tini` como PID 1 |
711
+
572
712
  ## Referência: `createApp(options)`
573
713
 
574
- | Opção | Padrão | Descrição |
575
- |---|---|---|
576
- | `bodyLimit` | `1048576` | Tamanho máximo do body JSON, em bytes |
577
- | `queryMaxKeys` | `1000` | Máximo de pares lidos da query string |
578
- | `requireSchemas` | `false` | Exige schemas de body e de resposta no registro das rotas |
579
- | `securityHeaders` | `'api'` | `'api'`, `'web'`, um objeto de opções, ou `false` para desligar |
580
- | `serializers` | — | Serializadores por content type, para responder em outros formatos |
581
- | `shutdown` | SIGINT/SIGTERM, 10s | `signals`, `timeoutMs`, `onShutdown`, `exit`; `false` desliga |
582
- | `trustProxy` | `false` | `true` ou o número de proxies: `ctx.ip`/`host`/`protocol` passam a usar os `X-Forwarded-*` |
583
- | `allowedHosts` | qualquer host válido | Lista de hosts (string ou RegExp); os demais recebem 400 |
584
- | `formatError` | Problem Details | `(problem, ctx) => corpo`, para personalizar o corpo de todos os erros |
585
- | `timeouts` | 30s / 10s / 5s | `requestTimeout`, `headersTimeout`, `keepAliveTimeout` |
586
- | `logger` | `consoleLogger()` | Logger compatível com pino; `false` desliga |
587
- | `logRequests` | `false` | Log de cada requisição concluída |
588
- | `requestId` | header `x-request-id` | `header`, `trustIncoming`, `generate` |
589
-
590
- Métodos do app: `use()`, `mount()`, `openapi()`, `openapiDocument()`, `onError()`, `onSend()`, `onResponse()`, `onClose()`, `listen()`, `close()`, `callback()`, `inject()`, `log`. O `mount()` devolve um `MountedGroup`, com os verbos do mapeamento manual e os mesmos métodos do app, para continuar o encadeamento.
714
+ | Opção | Padrão | Descrição |
715
+ | ----------------- | --------------------- | ------------------------------------------------------------------------------------------ |
716
+ | `bodyLimit` | `1048576` | Tamanho máximo do body JSON, em bytes |
717
+ | `queryMaxKeys` | `1000` | Máximo de pares lidos da query string |
718
+ | `requireSchemas` | `false` | Exige schemas de body e de resposta no registro das rotas |
719
+ | `securityHeaders` | `'api'` | `'api'`, `'web'`, um objeto de opções, ou `false` para desligar |
720
+ | `serializers` | — | Serializadores por content type, para responder em outros formatos |
721
+ | `shutdown` | SIGINT/SIGTERM, 10s | `signals`, `timeoutMs`, `drainDelayMs`, `onShutdown`, `exit`; `false` desliga |
722
+ | `protoPoisoning` | `'error'` | Body com `__proto__`/`constructor.prototype`: `'error'` (400) ou `'remove'` |
723
+ | `trustProxy` | `false` | `true` ou o número de proxies: `ctx.ip`/`host`/`protocol` passam a usar os `X-Forwarded-*` |
724
+ | `allowedHosts` | qualquer host válido | Lista de hosts (string ou RegExp); os demais recebem 400 |
725
+ | `formatError` | Problem Details | `(problem, ctx) => corpo`, para personalizar o corpo de todos os erros |
726
+ | `timeouts` | 30s / 10s / 5s | `requestTimeout`, `headersTimeout`, `keepAliveTimeout` |
727
+ | `logger` | `consoleLogger()` | Logger compatível com pino; `false` desliga |
728
+ | `logRequests` | `false` | Log de cada requisição concluída |
729
+ | `requestId` | header `x-request-id` | `header`, `trustIncoming`, `generate` |
730
+
731
+ Métodos do app: `use()`, `mount()`, `group()`, `health()`, `openapi()`, `openapiDocument()`, `onError()`, `onSend()`, `onResponse()`, `onClose()`, `listen()`, `close()`, `callback()`, `inject()`, `log`. O `mount()` devolve um `MountedGroup`, com os verbos do mapeamento manual e os mesmos métodos do app, para continuar o encadeamento.
591
732
 
592
733
  O body só é lido como JSON quando o `Content-Type` é `application/json` ou `application/*+json`.
593
734
 
@@ -620,10 +761,13 @@ pnpm install
620
761
  pnpm test # node:test + tsx
621
762
  pnpm test:coverage # com limites mínimos de cobertura
622
763
  pnpm typecheck
764
+ pnpm check # lint + formatação (Biome); pnpm format corrige a formatação
623
765
  pnpm build # dist/
624
766
  ```
625
767
 
626
- Mudanças notáveis estão no [CHANGELOG](CHANGELOG.md).
768
+ Para o `git blame` ignorar o commit de formatação: `git config blame.ignoreRevsFile .git-blame-ignore-revs`.
769
+
770
+ Mudanças notáveis estão no [CHANGELOG](CHANGELOG.md). Para reportar uma vulnerabilidade e conhecer a política de dependências, veja o [SECURITY.md](SECURITY.md).
627
771
 
628
772
  ## Licença
629
773