@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.4

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 (124) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +12 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/sse.md CHANGED
@@ -70,12 +70,12 @@ ce que fait une porte MCP.
70
70
  >
71
71
  > Ce plafond vaut pour l'origine que **voit le navigateur**. Derrière nginx, HAProxy ou un
72
72
  > ingress qui termine le TLS, cette origine est le proxy : c'est **lui** qui doit servir HTTP/2,
73
- > et `proxy:generate` le pose déjà (`http2 on`, `generateProxyConfig.ts:270`). Nodefony reçoit
74
- > alors du HTTP/1.1 (`proxy_http_version 1.1`, `generateProxyConfig.ts:351`) — y activer HTTP/2
73
+ > et `proxy:generate` le pose déjà (`http2 on`, `generateProxyConfig.ts#nginxServerBlock`). Nodefony reçoit
74
+ > alors du HTTP/1.1 (`proxy_http_version 1.1`, `generateProxyConfig.ts#nginxServerBlock`) — y activer HTTP/2
75
75
  > n'y change rien. Sans proxy, c'est Nodefony qui sert HTTP/2.
76
76
  >
77
77
  > De chaque côté du proxy, un flux est une connexion qui se tait entre deux événements. Le
78
- > battement (`SSE_HEARTBEAT_MS`, 15 s, `SseStream.ts:77`) la garde vivante ; le délai
78
+ > battement (`SSE_HEARTBEAT_MS`, 15 s, `SseStream.ts#SSE_HEARTBEAT_MS`) la garde vivante ; le délai
79
79
  > d'inactivité généré (300 s au moins) en tient vingt. Le seul piège réel : une action qui éteint
80
80
  > le battement (`heartbeat: false`) et se tait plus longtemps que ce délai se fait couper par le
81
81
  > proxy. Le guide [reverse-proxy](../../../../../docs/guides/reverse-proxy.md) détaille le reste.
@@ -142,23 +142,27 @@ data: {"step":"écriture"}
142
142
 
143
143
  - Les en-têtes partent dès `renderSse()` : `Content-Type: text/event-stream; charset=utf-8`,
144
144
  `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no` (`openSseStream()`,
145
- `SseStream.ts:427`).
145
+ `SseStream.ts#openSseStream`). Ce dernier demande à un proxy de type nginx de ne pas garder le
146
+ flux en tampon. À travers le frontal que génère `nodefony proxy:generate nginx`, chaque
147
+ événement arrive à son émission — mesuré par le scénario `edge` du banc de publication. La même
148
+ mesure, en-tête retiré, reste étalée sur nginx 1.30 : l'en-tête n'y est pas ce qui tient le
149
+ flux, il protège les proxys qui retiennent.
146
150
  - `await sse.send()` freine l'action si le client lit lentement — c'est la contre-pression.
147
151
 
148
152
  ## ⚙️ Les deux bouts de l'API
149
153
 
150
154
  ### Côté serveur — `this.renderSse()` et l'objet `SseStream`
151
155
 
152
- | Membre | Rôle |
153
- | ------------------------------- | -------------------------------------------------------------------------------------------------- |
154
- | `this.renderSse(options?)` | ouvre le flux (`Controller.renderSse()`, `Controller.ts:665`) ; lève sur WebSocket |
155
- | `this.acceptsSse()` | le client a-t-il demandé `text/event-stream` ? (`Controller.acceptsSse()`, `Controller.ts:649`) |
156
- | `sse.send(data, { event, id })` | un événement ; texte tel quel, objet en JSON (`SseStream.send()`, `SseStream.ts:260`) |
157
- | `sse.comment(text)` | un commentaire, ignoré par le client |
158
- | `sse.retry(ms)` | impose le délai de reconnexion du client |
159
- | `sse.onClose(fn)` | appelé à la fermeture, quel que soit le bout qui ferme (`SseStream.onClose()`, `SseStream.ts:306`) |
160
- | `sse.close()` | termine la requête, idempotent (`SseStream.close()`, `SseStream.ts:321`) |
161
- | `sse.closed` | le flux est-il fermé — à tester dans une boucle longue |
156
+ | Membre | Rôle |
157
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
158
+ | `this.renderSse(options?)` | ouvre le flux (`Controller.renderSse()`, `Controller.ts#Controller.renderSse`) ; lève sur WebSocket |
159
+ | `this.acceptsSse()` | le client a-t-il demandé `text/event-stream` ? (`Controller.acceptsSse()`, `Controller.ts#Controller.acceptsSse`) |
160
+ | `sse.send(data, { event, id })` | un événement ; texte tel quel, objet en JSON (`SseStream.send()`, `SseStream.ts#SseStream.send`) |
161
+ | `sse.comment(text)` | un commentaire, ignoré par le client |
162
+ | `sse.retry(ms)` | impose le délai de reconnexion du client |
163
+ | `sse.onClose(fn)` | appelé à la fermeture, quel que soit le bout qui ferme (`SseStream.onClose()`, `SseStream.ts#SseStream.onClose`) |
164
+ | `sse.close()` | termine la requête, idempotent (`SseStream.close()`, `SseStream.ts#SseStream.close`) |
165
+ | `sse.closed` | le flux est-il fermé — à tester dans une boucle longue |
162
166
 
163
167
  Options de `renderSse()` :
164
168
 
@@ -185,13 +189,13 @@ async runTask() {
185
189
  }
186
190
  ```
187
191
 
188
- `acceptsEventStream()` (`SseStream.ts:395`) ne retient `text/event-stream` que **nommé** avec un
192
+ `acceptsEventStream()` (`SseStream.ts#acceptsEventStream`) ne retient `text/event-stream` que **nommé** avec un
189
193
  poids non nul : `*/*` ne suffit pas — un client qui ne nomme pas le flux ne sait pas le lire —, et
190
194
  `q=0` est un refus (RFC 9110 §12.4.2).
191
195
 
192
196
  ### Côté client — `NodefonySse`
193
197
 
194
- `NodefonySse` (`NodefonySse.ts:110`) reprend le vocabulaire d'`EventSource` — `onopen`,
198
+ `NodefonySse` (`NodefonySse.ts#NodefonySse`) reprend le vocabulaire d'`EventSource` — `onopen`,
195
199
  `onmessage`, `onerror`, `addEventListener(type)`, `readyState`, `lastEventId`, `close()`,
196
200
  reconnexion avec `Last-Event-ID` — et ajoute ce que le natif refuse : `method`, `headers` (un
197
201
  `Authorization: Bearer`), `body`. Il est bâti sur `fetch`, donc tourne aussi sous Node, où
@@ -205,7 +209,7 @@ reconnexion avec `Last-Event-ID` — et ajoute ce que le natif refuse : `method`
205
209
  | autre statut (204 compris), autre type | `error`, `CLOSED`, **sans** reconnexion (WHATWG §9.2.3) |
206
210
  | événement au-delà de `maxEventSize` (4 Mi) | `CLOSED` sans reconnexion |
207
211
 
208
- L'analyseur du format est `SseParser` (`SseParser.ts:61`), exporté par `nodefony/client` : c'est
212
+ L'analyseur du format est `SseParser` (`SseParser.ts#SseParser`), exporté par `nodefony/client` : c'est
209
213
  le même qui relit le flux dans les tests du serveur.
210
214
 
211
215
  ## 🏗️ Architecture interne — ce que fait l'ouverture
@@ -213,20 +217,20 @@ le même qui relit le flux dans les tests du serveur.
213
217
  1. **La réponse est prise** (`context.sended`) : plus rien ne peut répondre à la place du flux.
214
218
  2. **La session est sauvée** s'il y en a une — ses cookies partent avec les en-têtes.
215
219
  3. **Le transport se constate** : flux HTTP/2, ou réponse HTTP/1.1 ; ni l'un ni l'autre → 500,
216
- avant d'avoir rien écrit (`startSseStream()`, `SseStream.ts:485`).
220
+ avant d'avoir rien écrit (`startSseStream()`, `SseStream.ts#startSseStream`).
217
221
  4. **Les en-têtes partent** — `respond()` en HTTP/2, `flushHeaders()` en HTTP/1.1 — et le délai
218
222
  d'inactivité de la requête est **désarmé** : sans cela, le flux vivant prendrait un 408 au bout
219
223
  de 30 s.
220
224
  5. **Le battement démarre** sur un minuteur `unref()`.
221
225
 
222
- La fin, quel que soit le bout qui ferme, passe par **un seul** nettoyage (`SseStream.ts:254`) :
226
+ La fin, quel que soit le bout qui ferme, passe par **un seul** nettoyage (`SseStream.ts#SseStream.send`) :
223
227
  minuteur arrêté, écouteurs `close` et `drain` retirés, attente de `drain` libérée, puis les
224
228
  écouteurs `onClose`.
225
229
 
226
230
  ## 🔐 Sécurité
227
231
 
228
232
  - **Injection de flux** : `event` et `id` sont refusés s'ils contiennent CR, LF ou NUL
229
- (`singleLine()`, `SseStream.ts:105`) — une valeur venue d'un utilisateur ne peut pas clore
233
+ (`singleLine()`, `SseStream.ts#singleLine`) — une valeur venue d'un utilisateur ne peut pas clore
230
234
  l'événement pour en forger un autre. Les `data` sur plusieurs lignes sont découpés en autant de
231
235
  champs, jamais recopiés tels quels.
232
236
  - **Client** : un serveur qui n'envoie jamais de fin de ligne ferait grossir la mémoire du client
package/docs/upload.md CHANGED
@@ -42,7 +42,7 @@ coverageFiles: context/http/Request.ts,context/http/parser.ts,service/upload/upl
42
42
 
43
43
  Le corps d'une requête n'est **jamais** interprété par le serveur ni par le routeur : c'est
44
44
  `HttpRequest` qui, selon le `Content-Type`, choisit **un** parser et remplit deux emplacements que ton
45
- contrôleur consomme — `queryPost` (les champs) et `queryFile` (les fichiers).
45
+ contrôleur consomme — `body` (les champs) et `files` (les fichiers).
46
46
 
47
47
  ```mermaid
48
48
  flowchart TD
@@ -51,21 +51,25 @@ flowchart TD
51
51
  RT -->|"x-www-form-urlencoded"| Q["ParserQs"]
52
52
  RT -->|"application/xml · text/xml"| X["ParserXml"]
53
53
  RT -->|"multipart/form-data"| M["busboy → disque (temp)"]
54
- RT -->|"autre / brut"| B["Parser brut"]
55
- J --> QP["queryPost<br/>@Body() · this.queryPost"]
54
+ RT -->|"text/* · autre · sans Content-Type"| B["ParserRaw"]
55
+ J --> QP["body<br/>@Body() · this.body"]
56
56
  Q --> QP
57
57
  X --> QP
58
- M --> QF["queryFile<br/>@UploadedFiles() · this.queryFile"]
58
+ M --> QF["files<br/>@UploadedFiles() · this.files"]
59
59
  M --> QP
60
- B --> DATA["request.data (Buffer)"]
60
+ B --> RAW["body : chaîne (text/*)<br/>ou octets Uint8Array (le reste)"]
61
61
  ```
62
62
 
63
63
  Trois idées portent tout le reste :
64
64
 
65
- 1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:502`)
66
- aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / parser brut selon l'en-tête.
65
+ 1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts#HttpRequest.parseRequest`)
66
+ aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / `ParserRaw` selon l'en-tête.
67
+ Ce dernier livre un corps qu'il ne décode pas **tel qu'il est reçu** : une chaîne pour `text/*`
68
+ (au `charset` déclaré), des **octets** (`Uint8Array`, sans copie) pour tout autre type et pour un
69
+ corps sans `Content-Type` (RFC 9110 §8.3 → `application/octet-stream`) — jamais un `{}` qui
70
+ mentirait. Un corps vide laisse `body` inchangé (`context/http/parser.ts#ParserRaw`).
67
71
  2. **Seul le multipart écrit sur disque.** Les fichiers sont **streamés** au fil de l'eau vers un
68
- fichier temporaire (`parseMultipart()`, `context/http/Request.ts:574`) — jamais bufferisés en RAM.
72
+ fichier temporaire (`parseMultipart()`, `context/http/Request.ts#HttpRequest.parseMultipart`) — jamais bufferisés en RAM.
69
73
  JSON / urlencoded / XML restent en mémoire (petits corps).
70
74
  3. **Deux budgets distincts, jamais confondus.** Le corps **non-multipart** est borné par `maxBodySize`
71
75
  (défaut **1 MiB**) ; le multipart a **ses propres** limites busboy (`maxFileSize`, `maxTotalFileSize`,
@@ -80,7 +84,7 @@ Trois idées portent tout le reste :
80
84
  | `multipart/form-data` | Format d'un formulaire qui contient des **fichiers** : le corps est découpé en _parts_ séparées (RFC 7578). |
81
85
  | Boundary | Chaîne-séparateur (`--…`) qui délimite chaque _part_ d'un corps multipart. |
82
86
  | Part | Un bloc d'un corps multipart : soit un **champ texte** (`field`), soit un **fichier** (`file`). |
83
- | Field / File | Champ texte (→ `queryPost`) vs fichier uploadé (→ `queryFile`), les deux issus du même corps multipart. |
87
+ | Field / File | Champ texte (→ `body`) vs fichier uploadé (→ `files`), les deux issus du même corps multipart. |
84
88
  | busboy | La bibliothèque de parsing multipart **en flux** (`@fastify/busboy`) : elle lit le corps sans le charger en RAM. |
85
89
  | Streaming (au fil de l'eau) | Écrire le fichier sur disque **pendant** sa réception, octet par octet — au lieu d'attendre le corps entier. |
86
90
  | Fichier temporaire (temp) | Le fichier écrit par busboy dans `uploadDir`, nommé par un UUID. Le contrôleur le déplace ou le laisse expirer. |
@@ -99,13 +103,13 @@ Imagine un **guichet de dépôt**. Le client arrive avec une enveloppe (`Content
99
103
  formulaire à plat (JSON, urlencodé, XML) que le guichetier **recopie sur une fiche** en mémoire, soit un
100
104
  **colis** (multipart) qu'il pose **directement dans une consigne** (le disque) sans jamais le tenir à
101
105
  bout de bras. Dans les deux cas, ton contrôleur ne voit pas l'enveloppe : il reçoit la fiche
102
- (`queryPost`) et le bordereau de consigne (`queryFile`).
106
+ (`body`) et le bordereau de consigne (`files`).
103
107
 
104
108
  Concrètement, la couche a quatre responsabilités, et rien d'autre :
105
109
 
106
110
  1. **Choisir le parser** d'après le `Content-Type`.
107
- 2. **Décoder les champs** (JSON, urlencoded, XML) → `queryPost`, lus par `@Body()`.
108
- 3. **Streamer les fichiers** multipart vers le disque, sans pic RAM → `queryFile`, lus par
111
+ 2. **Décoder les champs** (JSON, urlencoded, XML) → `body`, lus par `@Body()`.
112
+ 3. **Streamer les fichiers** multipart vers le disque, sans pic RAM → `files`, lus par
109
113
  `@UploadedFiles()`.
110
114
  4. **Borner la taille** — rejeter en `413` un corps qui menace la mémoire ou le disque.
111
115
 
@@ -117,23 +121,23 @@ Trois choix structurent l'implémentation, et chacun a une raison de sécurité
117
121
 
118
122
  **Le multipart ne touche jamais la RAM.** Là où l'ancien chemin bufferisait le corps entier, busboy lit
119
123
  le flux et écrit chaque fichier au fil de l'eau dans le dossier temporaire (`streamMultipart()`,
120
- `context/http/Request.ts:615`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
124
+ `context/http/Request.ts#HttpRequest.streamMultipart`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
121
125
  champs texte restent en mémoire. C'est ce qui rend un endpoint d'upload public tenable.
122
126
 
123
127
  **Le nom du fichier temporaire n'est jamais celui du client.** Chaque fichier reçu est écrit sous un nom
124
- `randomUUID()` + extension d'origine (`context/http/Request.ts:672`) : un nom malveillant
128
+ `randomUUID()` + extension d'origine (`context/http/Request.ts#HttpRequest.streamMultipart`) : un nom malveillant
125
129
  (`../../etc/passwd`) ne peut pas influencer le **chemin** d'écriture. Le nom d'origine est conservé en
126
130
  **métadonnée** (`filename`), pas dans le chemin.
127
131
 
128
132
  **Deux budgets, secure-by-default.** Le corps non-multipart est plafonné à **1 MiB** par défaut
129
- (`maxBodySize`, `http/nodefony/config/config.ts:1208`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
133
+ (`maxBodySize`, `http/nodefony/config/config.ts#httpConfigSchema`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
130
134
  multipart, lui, a ses propres bornes busboy (par fichier, cumul, nombre) qui coupent le flux et
131
- nettoient les temporaires déjà posés au moindre dépassement (`context/http/Request.ts:481`).
135
+ nettoient les temporaires déjà posés au moindre dépassement (`context/http/Request.ts#HttpRequest.enforceBodyLimit`).
132
136
 
133
137
  > [!IMPORTANT]
134
138
  > `@nodefony/http` ne peut pas importer `@nodefony/framework` (cycle). Les **décorateurs**
135
139
  > (`@Body`, `@UploadedFiles`…) vivent donc dans `@nodefony/framework`, mais ils lisent les emplacements
136
- > (`queryPost`, `queryFile`) remplis par `@nodefony/http`. Une page, deux modules — c'est le prix du
140
+ > (`body`, `files`) remplis par `@nodefony/http`. Une page, deux modules — c'est le prix du
137
141
  > découplage.
138
142
 
139
143
  ## 🚀 Démarrage rapide
@@ -143,30 +147,29 @@ n'écris que tes bornes (si les défauts ne conviennent pas) et le contrôleur q
143
147
 
144
148
  ### 1. Les bornes de payload
145
149
 
146
- Réglées sur le module `@nodefony/http`, colocalisées dans le manifeste via `use()`. Toutes ces clés
147
- sont facultatives : ce sont les défauts du schéma, écrits ici pour les rendre visibles et les resserrer.
150
+ Réglées sur le module `@nodefony/http`, dans son fragment `nodefony/config/http.ts` (que le manifeste
151
+ passe à `use()`). Toutes ces clés sont facultatives : ce sont les défauts du schéma, écrits ici pour
152
+ les rendre visibles et les resserrer.
148
153
 
149
154
  ```typescript
150
- // nodefony.config.ts — bornes de payload de l'application
151
- export default defineConfig(() => ({
152
- modules: [
153
- use("@nodefony/http", {
154
- // Corps NON-multipart (JSON, urlencoded, XML, brut) : 1 MiB par défaut.
155
- // Au-delà → 413 « Content Too Large ». 0 = illimité.
156
- maxBodySize: 1_048_576,
157
- upload: {
158
- // Chaque fichier (busboy limits.fileSize) — dépassement → 413.
159
- maxFileSize: 10 * 1024 * 1024, // 10 MiB
160
- // Cumul de TOUS les fichiers d'une même requête (compteur Nodefony).
161
- maxTotalFileSize: 30 * 1024 * 1024, // 30 MiB
162
- maxFiles: 10, // nombre de fichiers — anti-DoS
163
- // Intégrité optionnelle : hash calculé PENDANT le stream (0 relecture).
164
- hashAlgorithm: "sha256",
165
- },
166
- }),
167
- "@nodefony/framework",
168
- ],
169
- }));
155
+ // nodefony/config/http.ts — bornes de payload de l'application
156
+ import type { IHttpConfigInput } from "@nodefony/http";
157
+
158
+ export const httpConfig = () =>
159
+ ({
160
+ // Corps NON-multipart (JSON, urlencoded, XML, brut) : 1 MiB par défaut.
161
+ // Au-delà → 413 « Content Too Large ». 0 = illimité.
162
+ maxBodySize: 1_048_576,
163
+ upload: {
164
+ // Chaque fichier (busboy limits.fileSize) — dépassement → 413.
165
+ maxFileSize: 10 * 1024 * 1024, // 10 MiB
166
+ // Cumul de TOUS les fichiers d'une même requête (compteur Nodefony).
167
+ maxTotalFileSize: 30 * 1024 * 1024, // 30 MiB
168
+ maxFiles: 10, // nombre de fichiers — anti-DoS
169
+ // Intégrité optionnelle : hash calculé PENDANT le stream (0 relecture).
170
+ hashAlgorithm: "sha256",
171
+ },
172
+ }) satisfies IHttpConfigInput;
170
173
  ```
171
174
 
172
175
  ### 2. Le contrôleur qui reçoit l'upload
@@ -232,7 +235,7 @@ curl -k -X POST -H "content-type: application/json" \
232
235
  ## 🏗️ Architecture interne — le trajet d'un corps multipart
233
236
 
234
237
  Une requête multipart traverse `HttpRequest` → busboy → disque → `UploadService`, puis atterrit dans le
235
- contrôleur avec `queryFile`/`queryPost` déjà remplis.
238
+ contrôleur avec `files`/`body` déjà remplis.
236
239
 
237
240
  ```mermaid
238
241
  sequenceDiagram
@@ -246,30 +249,30 @@ sequenceDiagram
246
249
  R->>R: parseRequest() — content-type = multipart
247
250
  R->>B: request.pipe(busboy)
248
251
  B->>D: chaque fichier → <uuid>.<ext> (au fil de l'eau)
249
- B-->>R: field (texte) → queryPost
252
+ B-->>R: field (texte) → body
250
253
  Note over B,D: limites fileSize / total / files → 413 + cleanup des temp
251
254
  B->>R: finish
252
255
  R->>U: createUploadFile() → UploadedFile (stat async)
253
- R->>Ctrl: onRequestEnd — queryFile / queryPost prêts
256
+ R->>Ctrl: onRequestEnd — files / body prêts
254
257
  Ctrl->>D: file.moveAsync(dest)
255
258
  ```
256
259
 
257
260
  Les points d'implémentation qui expliquent des comportements surprenants :
258
261
 
259
262
  1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
260
- (`context/http/Request.ts:502`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
261
- des méthodes parsées (`context/http/Request.ts:94`) — l'oubli laissait tout `PATCH` avec un corps vide.
263
+ (`context/http/Request.ts#HttpRequest.parseRequest`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
264
+ des méthodes parsées (`context/http/Request.ts#parse`) — l'oubli laissait tout `PATCH` avec un corps vide.
262
265
  2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
263
- (`context/http/Request.ts:615`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
264
- qu'une fois tous les fichiers fermés (`bb.on("finish")`, `context/http/Request.ts:707`).
266
+ (`context/http/Request.ts#HttpRequest.streamMultipart`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
267
+ qu'une fois tous les fichiers fermés (`bb.on("finish")`, `context/http/Request.ts#HttpRequest.streamMultipart`).
265
268
  3. **Une limite dépassée nettoie les temporaires déjà posés** — `abort()`
266
- (`context/http/Request.ts:646`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
267
- déjà écrits (`context/http/Request.ts:64`) avant de rejeter en `413` : pas d'orphelins sur le disque.
269
+ (`context/http/Request.ts#HttpRequest.streamMultipart`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
270
+ déjà écrits (`context/http/Request.ts#unlinkQuiet`) avant de rejeter en `413` : pas d'orphelins sur le disque.
268
271
  4. **Les autres formats drainent AVANT de concaténer** — la base `Parser.parse()` attend `end`
269
- (`context/http/parser.ts:111`) avant `Buffer.concat` : sans ce drain, `ParserQs`/`ParserXml`
270
- lisaient un corps partiel → `queryPost` vide (bug de régression, cf tests).
272
+ (`context/http/parser.ts#Parser.parse`) avant `Buffer.concat` : sans ce drain, `ParserQs`/`ParserXml`
273
+ lisaient un corps partiel → `body` vide (bug de régression, cf tests).
271
274
  5. **Un multipart sans boundary exploitable ne crashe pas** — `new Busboy()` lève **synchroniquement** ;
272
- c'est rattrapé et on bascule sur le `Parser` brut (`context/http/Request.ts:417`).
275
+ c'est rattrapé et on bascule sur le `Parser` brut (`context/http/Request.ts#HttpRequest.parser`).
273
276
 
274
277
  ## ⚙️ Configuration — deux budgets, jamais confondus
275
278
 
@@ -283,10 +286,10 @@ non-multipart n'écoute pas `upload.*`.
283
286
  | `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
284
287
 
285
288
  Deux rideaux, tous deux `runtimeMutable` (éditable à chaud) : un **pré-check** sur `Content-Length`
286
- qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:479`), puis un **compteur
289
+ qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts#HttpRequest.enforceBodyLimit`), puis un **compteur
287
290
  en streaming** qui coupe le socket si le corps déborde sans `Content-Length` honnête — chunked ou
288
- menteur (`Parser.write()`, `context/http/parser.ts:33`, dépassement `context/http/parser.ts:33`).
289
- Champ `maxBodySize` du schéma : `config/config.ts:1208`.
291
+ menteur (`Parser.write()`, `context/http/parser.ts#Parser.write`, dépassement `context/http/parser.ts#Parser.write`).
292
+ Champ `maxBodySize` du schéma : `config/config.ts#httpConfigSchema`.
290
293
 
291
294
  **Par requête** : `maxBodySize` et les limites `upload.*` (hors `uploadDir`) sont sur la liste blanche
292
295
  du calque de configuration — une application peut les changer pour une requête
@@ -296,22 +299,22 @@ sont lus au moment de lire le corps (`Request.readBodyLimits()`), donc un calque
296
299
 
297
300
  ### Fichiers multipart — `upload.*`
298
301
 
299
- Table dérivée de `uploadSchema` (`config/config.ts:159`). Toutes les bornes sont `runtimeMutable`.
302
+ Table dérivée de `uploadSchema` (`config/config.ts#uploadSchema`). Toutes les bornes sont `runtimeMutable`.
300
303
 
301
- | Option | Type | Défaut | Effet |
302
- | ------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
303
- | `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts:162`). |
304
- | `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts:171`). |
305
- | `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts:183`). |
306
- | `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts:195`). |
307
- | `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts:206`). |
308
- | `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts:217`). |
309
- | `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts:228`). |
310
- | `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts:235`). |
304
+ | Option | Type | Défaut | Effet |
305
+ | ------------------ | -------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
306
+ | `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts#uploadSchema`). |
307
+ | `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts#uploadSchema`). |
308
+ | `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts#uploadSchema`). |
309
+ | `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts#uploadSchema`). |
310
+ | `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts#uploadSchema`). |
311
+ | `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts#uploadSchema`). |
312
+ | `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts#uploadSchema`). |
313
+ | `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts#uploadSchema`). |
311
314
 
312
315
  > [!WARNING]
313
316
  > `uploadDir` vide est **résolu au boot** sur le répertoire temporaire du kernel par le builder de config
314
- > (`defineModuleConfig.ts:43`) : un défaut vide n'est jamais un chemin vide en runtime. Ne dérérérence
317
+ > (`defineModuleConfig.ts#applyKernelDefaults`) : un défaut vide n'est jamais un chemin vide en runtime. Ne dérérérence
315
318
  > **jamais** le kernel au top-level d'un `config.ts` — c'est la raison du champ marqué `kernelDerived`.
316
319
 
317
320
  ## 🧰 API — accéder aux champs et aux fichiers
@@ -320,34 +323,34 @@ Depuis un contrôleur, deux surfaces équivalentes : les **décorateurs de param
320
323
  recommandé) et les **getters** de `Controller` (impératif). Les signatures exactes vivent dans
321
324
  `.ai/symbols.json` — jamais recopiées ici.
322
325
 
323
- | Accès | Source lue | Ancrage |
324
- | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
325
- | `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1312`) |
326
- | `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1311`) |
327
- | `@Body() body` | tous les champs parsés (`queryPost`) | `resolveParamArg` `"body"` (`routerDecorators.ts:1232`) |
328
- | `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1279`) |
329
- | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1358`) |
330
- | `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:354`) |
331
- | `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:364`) |
326
+ | Accès | Source lue | Ancrage |
327
+ | -------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------- |
328
+ | `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`files`) | `resolveParamArg` `"files"` (`routerDecorators.ts#UploadedFiles`) |
329
+ | `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts#UploadedFile`) |
330
+ | `@Body() body` | tous les champs parsés (`body`) | `resolveParamArg` `"body"` (`routerDecorators.ts#Body`) |
331
+ | `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts#Body`) |
332
+ | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts#resolveParamArg`) |
333
+ | `this.files` | équivalent getter des fichiers | `Controller.files` (`framework/nodefony/src/Controller.ts#Controller.files`) |
334
+ | `this.body` | équivalent getter des champs | `Controller.body` (`framework/nodefony/src/Controller.ts#Controller.body`) |
332
335
 
333
336
  Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
334
- (`routerDecorators.ts:1312`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
335
- `IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
337
+ (`routerDecorators.ts#UploadedFiles`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
338
+ `IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts#IUploadedFile`, `interfaces/IUpload.ts#IParsedUploadFile`).
336
339
 
337
340
  ### Un fichier uploadé — `UploadedFile`
338
341
 
339
- Chaque entrée de `queryFile` est un `UploadedFile` (`service/upload/upload-service.ts:96`), déjà écrit
342
+ Chaque entrée de `files` est un `UploadedFile` (`service/upload/upload-service.ts#UploadedFile`), déjà écrit
340
343
  sur disque. Ses membres utiles :
341
344
 
342
- | Membre | Ce qu'il donne | Ancrage |
343
- | ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
344
- | `filename` | Nom **d'origine** déclaré par le client (métadonnée, jamais le chemin). | `realName()` (`service/upload/upload-service.ts:147`) |
345
- | `size` | Taille réellement écrite (octets). | `getSize()` (`service/upload/upload-service.ts:139`) |
346
- | `prettySize` | Taille lisible (`« 1.2 MB »`). | `getPrettySize()` (`service/upload/upload-service.ts:143`) |
347
- | `mimeType` | Type MIME déclaré (`image/png`…), sinon deviné de l'extension. | `getMimeType()` (`service/upload/upload-service.ts:153`) |
348
- | `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts:55` |
349
- | `moveAsync(target)` | Déplace le temp — **non bloquant, recommandé** dans le pipeline. | `moveAsync()` (`service/upload/upload-service.ts:218`) |
350
- | `move(target)` | Variante **synchrone** (compat) — bloque l'event-loop. | `move()` (`service/upload/upload-service.ts:186`) |
345
+ | Membre | Ce qu'il donne | Ancrage |
346
+ | ------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
347
+ | `filename` | Nom **d'origine** déclaré par le client (métadonnée, jamais le chemin). | `realName()` (`service/upload/upload-service.ts#UploadedFile.realName`) |
348
+ | `size` | Taille réellement écrite (octets). | `getSize()` (`service/upload/upload-service.ts#UploadedFile.getSize`) |
349
+ | `prettySize` | Taille lisible (`« 1.2 MB »`). | `getPrettySize()` (`service/upload/upload-service.ts#UploadedFile.getPrettySize`) |
350
+ | `mimeType` | Type MIME déclaré (`image/png`…), sinon deviné de l'extension. | `getMimeType()` (`service/upload/upload-service.ts#UploadedFile.getMimeType`) |
351
+ | `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts#IUploadedFile.hashAlgorithm` |
352
+ | `moveAsync(target)` | Déplace le temp — **non bloquant, recommandé** dans le pipeline. | `moveAsync()` (`service/upload/upload-service.ts#UploadedFile.moveAsync`) |
353
+ | `move(target)` | Variante **synchrone** (compat) — bloque l'event-loop. | `move()` (`service/upload/upload-service.ts#UploadedFile.move`) |
351
354
 
352
355
  `move`/`moveAsync` acceptent un **fichier cible** ou un **dossier existant** : sur un dossier, la
353
356
  destination est bâtie avec `filename` — d'où l'avertissement de sécurité ci-dessous.
@@ -357,7 +360,7 @@ destination est bâtie avec `filename` — d'où l'avertissement de sécurité c
357
360
  Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
358
361
  par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
359
362
  `Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
360
- (`routerDecorators.ts:1509`), mémoïsé sur la route.
363
+ (`routerDecorators.ts#routeExpectsBodyStream`), mémoïsé sur la route.
361
364
 
362
365
  ```ts
363
366
  // fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
@@ -376,15 +379,15 @@ piégé. Les défenses en place, et **ce qui reste à ta charge**.
376
379
  <!-- prettier-ignore -->
377
380
  | Menace | Défense côté framework | À ta charge |
378
381
  | --- | --- | --- |
379
- | **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:672`). | La **destination** de `move()` (voir avertissement). |
382
+ | **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts#HttpRequest.streamMultipart`). | La **destination** de `move()` (voir avertissement). |
380
383
  | **Saturation RAM** | Multipart streamé (jamais bufferisé) ; corps non-multipart borné (`maxBodySize`). | Resserrer `maxBodySize` selon l'endpoint. |
381
- | **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts:646`). | Purger les temp non déplacés (TTL / cron). |
382
- | **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts:754`). | — |
384
+ | **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts#HttpRequest.streamMultipart`). | Purger les temp non déplacés (TTL / cron). |
385
+ | **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts#HttpRequest.streamMultipart`). | — |
383
386
  | **Type de fichier hostile** | `mimeType` **déclaré** est exposé tel quel. | Valider le type/contenu réel (le MIME client est déclaratif). |
384
387
 
385
388
  > [!WARNING]
386
389
  > **Path traversal sur la destination.** `move(dir)` / `moveAsync(dir)` vers un **dossier** construit le
387
- > chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts:101`). Un nom
390
+ > chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts#UploadedFile.filename`). Un nom
388
391
  > `../../etc/cron.d/x` s'échapperait du dossier. Le framework protège le chemin du **temporaire**, pas
389
392
  > celui que **tu** choisis. Règle : passe une **cible complète** que tu contrôles, ou assainis toujours
390
393
  > avec `path.basename(file.filename)` — exactement le `safeName` du Démarrage rapide.
@@ -394,14 +397,14 @@ piégé. Les défenses en place, et **ce qui reste à ta charge**.
394
397
  Le parsing du corps est sur le chemin de chaque requête écrivante — les choix visibles dans le code :
395
398
 
396
399
  - **Multipart en flux pur** — plus de double-bufferisation : busboy écrit sur disque au fil de l'eau,
397
- seuls les champs texte restent en mémoire (`context/http/Request.ts:400`).
400
+ seuls les champs texte restent en mémoire (`context/http/Request.ts#HttpRequest.initialize`).
398
401
  - **Rejet AVANT lecture** — le pré-check `Content-Length` renvoie `413` sans lire un octet
399
- (`context/http/Request.ts:466`) ; le compteur streaming **libère immédiatement** la RAM déjà
400
- bufferisée au dépassement (`context/http/parser.ts:43`).
402
+ (`context/http/Request.ts#HttpRequest.readBodyLimits`) ; le compteur streaming **libère immédiatement** la RAM déjà
403
+ bufferisée au dépassement (`context/http/parser.ts#Parser.write`).
401
404
  - **`stat` non bloquant** — `UploadedFile.create()` résout les stats du fichier en async
402
- (`service/upload/upload-service.ts:130`), plus de `lstatSync` par fichier uploadé.
405
+ (`service/upload/upload-service.ts#UploadedFile.create`), plus de `lstatSync` par fichier uploadé.
403
406
  - **Listeners jumeaux nettoyés** — le drain de fin de corps retire ses écouteurs `end`/`error`/overflow
404
- à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts:78`).
407
+ à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts#Parser.ended`).
405
408
  - **Hash opt-in** — `hashAlgorithm: false` par défaut : zéro coût CPU tant que l'intégrité n'est pas
406
409
  demandée.
407
410
 
@@ -410,28 +413,28 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
410
413
 
411
414
  ## 📜 Normes appliquées
412
415
 
413
- | Domaine | Norme | Ancrage |
414
- | ---------------------------------- | -------------------------------- | ------------------------------------------------------------- |
415
- | Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:574`) |
416
- | Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:479`) |
417
- | 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts:33`) |
418
- | Bornes multipart → 413 | RFC 9110 §15.5.14 | `stream.on("limit")` (`context/http/Request.ts:481`) |
419
- | Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts:672`) |
420
- | Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:875`) |
416
+ | Domaine | Norme | Ancrage |
417
+ | ---------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
418
+ | Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts#HttpRequest.parseMultipart`) |
419
+ | Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts#HttpRequest.enforceBodyLimit`) |
420
+ | 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts#Parser.write`) |
421
+ | Bornes multipart → 413 | RFC 9110 §15.5.14 | `stream.on("limit")` (`context/http/Request.ts#HttpRequest.enforceBodyLimit`) |
422
+ | Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts#HttpRequest.streamMultipart`) |
423
+ | Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts#HttpRequest.getCharset`) |
421
424
 
422
425
  ## ⚠️ Pièges
423
426
 
424
- | Symptôme | Cause | Correction |
425
- | ------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
426
- | `@Body()` est vide sur un upload de fichier | Les fichiers vont dans `queryFile`, **pas** dans `queryPost` | Lire les fichiers avec `@UploadedFiles()` / `this.queryFile` ; `@Body()` = champs texte |
427
- | Un `POST` JSON de 1,5 Mo est refusé en `413` | `maxBodySize` vaut **1 MiB** par défaut | Augmenter `maxBodySize` (pas `upload.maxFileSize`) |
428
- | Un gros fichier passe alors que `maxBodySize` est petit | Le multipart **n'écoute pas** `maxBodySize` | Régler `upload.maxFileSize` / `maxTotalFileSize` |
429
- | `413` sur upload sans message clair | Une borne busboy atteinte en streaming (fichier, cumul, nombre) | Vérifier les bornes `upload.*` — 413 émis sur `stream.on("limit")` (`context/http/Request.ts:481`) |
430
- | Un fichier écrit `../../etc/…` après un `move` | `move(dossier)` utilise le nom **client** (`upload-service.ts:197`) | Passer une cible complète, ou `path.basename(file.filename)` |
431
- | Des fichiers temporaires s'accumulent dans `uploadDir` | Le contrôleur ne déplace jamais le temp | Appeler `moveAsync()` (ou purger l'ancien temp par TTL) |
432
- | `queryPost` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts:93`) |
433
- | Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:875`) |
434
- | `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:521`) |
427
+ | Symptôme | Cause | Correction |
428
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
429
+ | `@Body()` est vide sur un upload de fichier | Les fichiers vont dans `files`, **pas** dans `body` | Lire les fichiers avec `@UploadedFiles()` / `this.files` ; `@Body()` = champs texte |
430
+ | Un `POST` JSON de 1,5 Mo est refusé en `413` | `maxBodySize` vaut **1 MiB** par défaut | Augmenter `maxBodySize` (pas `upload.maxFileSize`) |
431
+ | Un gros fichier passe alors que `maxBodySize` est petit | Le multipart **n'écoute pas** `maxBodySize` | Régler `upload.maxFileSize` / `maxTotalFileSize` |
432
+ | `413` sur upload sans message clair | Une borne busboy atteinte en streaming (fichier, cumul, nombre) | Vérifier les bornes `upload.*` — 413 émis sur `stream.on("limit")` (`context/http/Request.ts#HttpRequest.enforceBodyLimit`) |
433
+ | Un fichier écrit `../../etc/…` après un `move` | `move(dossier)` utilise le nom **client** (`upload-service.ts#UploadedFile.move`) | Passer une cible complète, ou `path.basename(file.filename)` |
434
+ | Des fichiers temporaires s'accumulent dans `uploadDir` | Le contrôleur ne déplace jamais le temp | Appeler `moveAsync()` (ou purger l'ancien temp par TTL) |
435
+ | `body` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts#parse`) |
436
+ | Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts#HttpRequest.getCharset`) |
437
+ | `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts#HttpRequest.parseRequest`) |
435
438
 
436
439
  ## 🧪 Tests & couverture
437
440
 
@@ -443,7 +446,7 @@ dans le Markdown).
443
446
  | Unitaires — `UploadedFile` | `unit/UploadedFile.test.ts` — `create()` async, `move`/`moveAsync`, `mimeType`, `realName` |
444
447
  | Unitaires — `@Body` stream | `framework` `unit/BodyStream.test.ts` — `@Body({ stream })`, `resolveParamArg`, `routeExpectsBodyStream` |
445
448
  | Intégration — multipart | `http/upload.test.ts` — upload simple, multi-fichiers, mime non-octet, champ + fichier, `413` par fichier et cumulé |
446
- | Intégration — content-types | `http/body-content-types.test.ts` — JSON, urlencoded, `@Body("champ")`, XML → `queryPost` |
449
+ | Intégration — content-types | `http/body-content-types.test.ts` — JSON, urlencoded, `@Body("champ")`, XML → `body` |
447
450
  | Intégration — bornes | `http/body-limit.test.ts` — sous la limite `200`, `Content-Length` > 1 MiB → `413`, chunked > 1 MiB refusé |
448
451
  | Intégration — flux brut | `integration/bodyStream.test.ts` — `@Body({ stream })` bout-en-bout (TLS + clair), non-régression `@Body()` |
449
452
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/http",
3
- "version": "10.0.0-beta.3",
3
+ "version": "10.0.0-beta.4",
4
4
  "description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "private": false,
29
29
  "engines": {
30
- "node": ">=24.0.0"
30
+ "node": ">=24.14.0"
31
31
  },
32
32
  "keywords": [
33
33
  "nodefony",
@@ -51,12 +51,11 @@
51
51
  "test": "./nodefony/tests"
52
52
  },
53
53
  "dependencies": {
54
- "@fastify/busboy": "3.2.3",
54
+ "@fastify/busboy": "3.2.4",
55
55
  "cookie": "2.0.1",
56
- "http-terminator": "3.2.0",
57
56
  "mime-types": "3.0.2",
58
57
  "ms": "^2.1.3",
59
- "qs": "^6.16.0",
58
+ "qs": "^6.16.1",
60
59
  "serve-static": "2.2.1",
61
60
  "tslib": "2.8.1",
62
61
  "ws": "8.22.0",
@@ -72,16 +71,16 @@
72
71
  "@types/websocket": "1.0.10",
73
72
  "@types/ws": "8.18.2",
74
73
  "@types/xml2js": "0.4.14",
75
- "@vitest/coverage-v8": "5.0.4",
76
- "nodefony": "^10.0.0-beta.3",
74
+ "@vitest/coverage-v8": "5.0.5",
75
+ "nodefony": "^10.0.0-beta.4",
77
76
  "tsx": "4.23.15",
78
- "vitest": "5.0.4"
77
+ "vitest": "5.0.5"
79
78
  },
80
79
  "license": "Apache-2.0",
81
80
  "readmeFilename": "README.md",
82
81
  "contributors": [],
83
82
  "peerDependencies": {
84
- "nodefony": "^10.0.0-beta.3",
83
+ "nodefony": "^10.0.0-beta.4",
85
84
  "zod": "^4.6.7"
86
85
  },
87
86
  "files": [
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
2
- function __decorateMetadata(k, v) {
3
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
- }
5
- //#endregion
6
- export { __decorateMetadata as default };