@nodefony/http 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
package/docs/upload.md ADDED
@@ -0,0 +1,460 @@
1
+ ---
2
+ title: "Upload & corps de requête — multipart, JSON, bornes de payload"
3
+ navTitle: "Upload & corps de requête"
4
+ lang: fr
5
+ module: "@nodefony/http"
6
+ topic: upload
7
+ section: "Cœur runtime"
8
+ audience: [developer]
9
+ tags:
10
+ [
11
+ upload,
12
+ multipart,
13
+ busboy,
14
+ body,
15
+ json,
16
+ urlencoded,
17
+ xml,
18
+ payload,
19
+ 413,
20
+ fichiers,
21
+ ]
22
+ version: "doc"
23
+ status: stable
24
+ updated: 2026-07-21
25
+ source: "src/packages/@nodefony/http/docs/upload.md"
26
+ coverageModule: http
27
+ coverageFiles: context/http/Request.ts,context/http/parser.ts,service/upload/upload-service.ts,interfaces/IUpload.ts,config/config.ts
28
+ ---
29
+
30
+ # Upload & corps de requête — multipart, JSON, bornes de payload
31
+
32
+ > Ce qui arrive après le `?` de l'URL : le **corps** d'une requête `POST`/`PUT`/`PATCH`/`DELETE`. Selon
33
+ > son `Content-Type`, Nodefony le parse de quatre façons — JSON, formulaire urlencodé, XML, ou
34
+ > **multipart** (les fichiers uploadés) — et le pose là où ton contrôleur le lit. Cette page décrit ce
35
+ > parsing, comment on accède aux **champs** et aux **fichiers**, l'API d'un fichier uploadé
36
+ > (`nom`, `taille`, `mimetype`, `move`), et les **deux budgets de taille** qui protègent le processus
37
+ > d'un corps trop gros. Chaque fait est ancré sur le code.
38
+
39
+ 📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Upload & corps de requête**
40
+
41
+ ## 🧠 Le modèle mental — un corps, quatre parsers, deux budgets
42
+
43
+ Le corps d'une requête n'est **jamais** interprété par le serveur ni par le routeur : c'est
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).
46
+
47
+ ```mermaid
48
+ flowchart TD
49
+ REQ["POST / PUT / PATCH / DELETE<br/>+ Content-Type"] --> RT["HttpRequest.parseRequest()"]
50
+ RT -->|"application/json · *+json"| J["ParserJson"]
51
+ RT -->|"x-www-form-urlencoded"| Q["ParserQs"]
52
+ RT -->|"application/xml · text/xml"| X["ParserXml"]
53
+ RT -->|"multipart/form-data"| M["busboy → disque (temp)"]
54
+ RT -->|"autre / brut"| B["Parser brut"]
55
+ J --> QP["queryPost<br/>@Body() · this.queryPost"]
56
+ Q --> QP
57
+ X --> QP
58
+ M --> QF["queryFile<br/>@UploadedFiles() · this.queryFile"]
59
+ M --> QP
60
+ B --> DATA["request.data (Buffer)"]
61
+ ```
62
+
63
+ Trois idées portent tout le reste :
64
+
65
+ 1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:429`)
66
+ aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / parser brut selon l'en-tête.
67
+ 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:499`) — jamais bufferisés en RAM.
69
+ JSON / urlencoded / XML restent en mémoire (petits corps).
70
+ 3. **Deux budgets distincts, jamais confondus.** Le corps **non-multipart** est borné par `maxBodySize`
71
+ (défaut **1 MiB**) ; le multipart a **ses propres** limites busboy (`maxFileSize`, `maxTotalFileSize`,
72
+ défaut **500 MB**). Régler l'un ne change rien à l'autre — c'est le piège n° 1.
73
+
74
+ ## 📖 Lexique
75
+
76
+ | Terme | Sens |
77
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
78
+ | Corps (body) | Les octets envoyés **après** les en-têtes d'une requête (le contenu d'un `POST`). |
79
+ | `Content-Type` | En-tête déclarant le format du corps (`application/json`, `multipart/form-data`…). Il décide du parser. |
80
+ | `multipart/form-data` | Format d'un formulaire qui contient des **fichiers** : le corps est découpé en _parts_ séparées (RFC 7578). |
81
+ | Boundary | Chaîne-séparateur (`--…`) qui délimite chaque _part_ d'un corps multipart. |
82
+ | 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. |
84
+ | busboy | La bibliothèque de parsing multipart **en flux** (`@fastify/busboy`) : elle lit le corps sans le charger en RAM. |
85
+ | 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
+ | Fichier temporaire (temp) | Le fichier écrit par busboy dans `uploadDir`, nommé par un UUID. Le contrôleur le déplace ou le laisse expirer. |
87
+ | `urlencoded` | `application/x-www-form-urlencoded` : un formulaire simple `a=1&b=2` (pas de fichier). |
88
+ | Payload | Synonyme de corps de requête, surtout quand on parle de sa **taille**. |
89
+ | 413 « Content Too Large » | Le statut HTTP renvoyé quand le corps dépasse une borne (RFC 9110 §15.5.14). |
90
+ | `Content-Length` | En-tête annonçant la taille du corps. Sert au pré-contrôle 413 **avant** de lire. |
91
+ | Chunked | Corps envoyé sans `Content-Length` (`Transfer-Encoding: chunked`) : la taille n'est connue qu'en cours de lecture. |
92
+ | Path traversal | Attaque où un nom de fichier (`../../etc/passwd`) fait écrire **hors** du dossier prévu. |
93
+ | Hash d'intégrité | Empreinte (`sha256`…) calculée pendant le stream, pour vérifier qu'un fichier n'a pas été altéré. |
94
+ | `Readable` | Un flux Node lisible. `@Body({ stream: true })` en injecte un — le corps brut, non parsé. |
95
+
96
+ ## Qu'est-ce qu'un upload, ici ?
97
+
98
+ Imagine un **guichet de dépôt**. Le client arrive avec une enveloppe (`Content-Type`) : dedans, soit un
99
+ formulaire à plat (JSON, urlencodé, XML) que le guichetier **recopie sur une fiche** en mémoire, soit un
100
+ **colis** (multipart) qu'il pose **directement dans une consigne** (le disque) sans jamais le tenir à
101
+ 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`).
103
+
104
+ Concrètement, la couche a quatre responsabilités, et rien d'autre :
105
+
106
+ 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
109
+ `@UploadedFiles()`.
110
+ 4. **Borner la taille** — rejeter en `413` un corps qui menace la mémoire ou le disque.
111
+
112
+ Le routage, le firewall, le contrôleur viennent après : ils reçoivent un corps déjà parsé et déjà borné.
113
+
114
+ ## La vision Nodefony
115
+
116
+ Trois choix structurent l'implémentation, et chacun a une raison de sécurité ou de performance.
117
+
118
+ **Le multipart ne touche jamais la RAM.** Là où l'ancien chemin bufferisait le corps entier, busboy lit
119
+ le flux et écrit chaque fichier au fil de l'eau dans le dossier temporaire (`streamMultipart()`,
120
+ `context/http/Request.ts:537`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
121
+ champs texte restent en mémoire. C'est ce qui rend un endpoint d'upload public tenable.
122
+
123
+ **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:594`) : un nom malveillant
125
+ (`../../etc/passwd`) ne peut pas influencer le **chemin** d'écriture. Le nom d'origine est conservé en
126
+ **métadonnée** (`filename`), pas dans le chemin.
127
+
128
+ **Deux budgets, secure-by-default.** Le corps non-multipart est plafonné à **1 MiB** par défaut
129
+ (`maxBodySize`, `http/nodefony/config/config.ts:1005`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
130
+ 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`).
132
+
133
+ > [!IMPORTANT]
134
+ > `@nodefony/http` ne peut pas importer `@nodefony/framework` (cycle). Les **décorateurs**
135
+ > (`@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
137
+ > découplage.
138
+
139
+ ## 🚀 Démarrage rapide
140
+
141
+ Dans une application générée par `nodefony create app`, le parsing du corps est **déjà branché** : tu
142
+ n'écris que tes bornes (si les défauts ne conviennent pas) et le contrôleur qui reçoit l'upload.
143
+
144
+ ### 1. Les bornes de payload
145
+
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.
148
+
149
+ ```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
+ }));
170
+ ```
171
+
172
+ ### 2. Le contrôleur qui reçoit l'upload
173
+
174
+ Le pipeline a déjà streamé les fichiers sur disque et parsé les champs texte. Le contrôleur les lit par
175
+ paramètres décorés — `@UploadedFiles()` pour les fichiers, `@Body("champ")` pour un champ — puis déplace
176
+ chaque fichier vers son emplacement définitif.
177
+
178
+ ```typescript
179
+ // nodefony/controller/UploadController.ts — reçoit un upload multipart
180
+ import {
181
+ Controller,
182
+ controller,
183
+ Post,
184
+ Body,
185
+ UploadedFiles,
186
+ } from "@nodefony/framework";
187
+ import type { ContextType, IUploadedFile } from "@nodefony/http";
188
+ import path from "node:path";
189
+
190
+ @controller("/avatars")
191
+ class UploadController extends Controller {
192
+ constructor(context: ContextType) {
193
+ super("UploadController", context);
194
+ }
195
+
196
+ // multipart/form-data : les fichiers arrivent DÉJÀ écrits en temp (streaming
197
+ // busboy), les champs texte via @Body(). @UploadedFile() donnerait le premier.
198
+ @Post("/")
199
+ async upload(
200
+ @UploadedFiles() files: IUploadedFile[] | undefined,
201
+ @Body("label") label: string | undefined,
202
+ ) {
203
+ const saved: Array<{ name: string; size: number }> = [];
204
+ for (const file of files ?? []) {
205
+ // SÉCURITÉ : le temp est nommé par un UUID (jamais le nom client). Pour la
206
+ // DESTINATION, on impose NOTRE nom — jamais file.filename brut (traversal).
207
+ const safeName = `${Date.now()}-${path.basename(file.filename)}`;
208
+ const dest = path.resolve("/var/app/uploads", safeName);
209
+ await file.moveAsync(dest); // variante non bloquante (recommandée)
210
+ saved.push({ name: file.filename, size: file.size });
211
+ }
212
+ return this.renderJson({ label: label ?? null, files: saved });
213
+ }
214
+ }
215
+
216
+ export default UploadController;
217
+ ```
218
+
219
+ ### 3. Vérifier depuis le terminal
220
+
221
+ ```bash
222
+ # multipart : un champ texte + un fichier, sur le port TLS
223
+ curl -k -F "label=profil" -F "file=@./photo.png" https://127.0.0.1:5152/avatars/
224
+ # {"label":"profil","files":[{"name":"photo.png","size":12345}]}
225
+
226
+ # corps JSON non-multipart > maxBodySize (1 MiB) → rejet avant lecture
227
+ curl -k -X POST -H "content-type: application/json" \
228
+ --data-binary @big.json https://127.0.0.1:5152/avatars/
229
+ # HTTP/2 413 (Request body too large)
230
+ ```
231
+
232
+ ## 🏗️ Architecture interne — le trajet d'un corps multipart
233
+
234
+ Une requête multipart traverse `HttpRequest` → busboy → disque → `UploadService`, puis atterrit dans le
235
+ contrôleur avec `queryFile`/`queryPost` déjà remplis.
236
+
237
+ ```mermaid
238
+ sequenceDiagram
239
+ participant C as Client
240
+ participant R as HttpRequest
241
+ participant B as busboy
242
+ participant D as disque (temp)
243
+ participant U as UploadService
244
+ participant Ctrl as Controller
245
+ C->>R: POST multipart/form-data
246
+ R->>R: parseRequest() — content-type = multipart
247
+ R->>B: request.pipe(busboy)
248
+ B->>D: chaque fichier → <uuid>.<ext> (au fil de l'eau)
249
+ B-->>R: field (texte) → queryPost
250
+ Note over B,D: limites fileSize / total / files → 413 + cleanup des temp
251
+ B->>R: finish
252
+ R->>U: createUploadFile() → UploadedFile (stat async)
253
+ R->>Ctrl: onRequestEnd — queryFile / queryPost prêts
254
+ Ctrl->>D: file.moveAsync(dest)
255
+ ```
256
+
257
+ Les points d'implémentation qui expliquent des comportements surprenants :
258
+
259
+ 1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
260
+ (`context/http/Request.ts:292`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
261
+ des méthodes parsées (`context/http/Request.ts:63`) — l'oubli laissait tout `PATCH` avec un corps vide.
262
+ 2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
263
+ (`context/http/Request.ts:537`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
264
+ qu'une fois tous les fichiers fermés (`context/http/Request.ts:538`).
265
+ 3. **Une limite dépassée nettoie les temporaires déjà posés** — `abort()`
266
+ (`context/http/Request.ts:557`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
267
+ déjà écrits (`context/http/Request.ts:568`) avant de rejeter en `413` : pas d'orphelins sur le disque.
268
+ 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).
271
+ 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:375`).
273
+
274
+ ## ⚙️ Configuration — deux budgets, jamais confondus
275
+
276
+ C'est la distinction la plus utile de cette page. **Le multipart n'écoute pas `maxBodySize`** ; le
277
+ non-multipart n'écoute pas `upload.*`.
278
+
279
+ ### Corps non-multipart — `maxBodySize`
280
+
281
+ | Option | Type | Défaut | Effet |
282
+ | ------------- | ------ | ------------------- | ------------------------------------------------------------------------------ |
283
+ | `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
284
+
285
+ Deux rideaux, tous deux `runtimeMutable` (éditable à chaud) : un **pré-check** sur `Content-Length`
286
+ qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:296`), puis un **compteur
287
+ 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:44`).
289
+ Champ `maxBodySize` du schéma : `config/config.ts:1005`.
290
+
291
+ ### Fichiers multipart — `upload.*`
292
+
293
+ Table dérivée de `uploadSchema` (`config/config.ts:144`). Toutes les bornes sont `runtimeMutable`.
294
+
295
+ | Option | Type | Défaut | Effet |
296
+ | ------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
297
+ | `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts:146`). |
298
+ | `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts:155`). |
299
+ | `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts:167`). |
300
+ | `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts:179`). |
301
+ | `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts:190`). |
302
+ | `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts:201`). |
303
+ | `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts:212`). |
304
+ | `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts:219`). |
305
+
306
+ > [!WARNING]
307
+ > `uploadDir` vide est **résolu au boot** sur le répertoire temporaire du kernel par le builder de config
308
+ > (`defineModuleConfig.ts:43`) : un défaut vide n'est jamais un chemin vide en runtime. Ne dérérérence
309
+ > **jamais** le kernel au top-level d'un `config.ts` — c'est la raison du champ marqué `kernelDerived`.
310
+
311
+ ## 🧰 API — accéder aux champs et aux fichiers
312
+
313
+ Depuis un contrôleur, deux surfaces équivalentes : les **décorateurs de paramètre** (déclaratif,
314
+ recommandé) et les **getters** de `Controller` (impératif). Les signatures exactes vivent dans
315
+ `.ai/symbols.json` — jamais recopiées ici.
316
+
317
+ | Accès | Source lue | Ancrage |
318
+ | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
319
+ | `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1240`) |
320
+ | `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1239`) |
321
+ | `@Body() body` | tous les champs parsés (`queryPost`) | `resolveParamArg` `"body"` (`routerDecorators.ts:1178`) |
322
+ | `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1178`) |
323
+ | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1227`) |
324
+ | `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:205`) |
325
+ | `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:214`) |
326
+
327
+ Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
328
+ (`routerDecorators.ts:1240`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
329
+ `IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
330
+
331
+ ### Un fichier uploadé — `UploadedFile`
332
+
333
+ Chaque entrée de `queryFile` est un `UploadedFile` (`service/upload/upload-service.ts:96`), déjà écrit
334
+ sur disque. Ses membres utiles :
335
+
336
+ | Membre | Ce qu'il donne | Ancrage |
337
+ | ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
338
+ | `filename` | Nom **d'origine** déclaré par le client (métadonnée, jamais le chemin). | `realName()` (`service/upload/upload-service.ts:147`) |
339
+ | `size` | Taille réellement écrite (octets). | `getSize()` (`service/upload/upload-service.ts:139`) |
340
+ | `prettySize` | Taille lisible (`« 1.2 MB »`). | `getPrettySize()` (`service/upload/upload-service.ts:143`) |
341
+ | `mimeType` | Type MIME déclaré (`image/png`…), sinon deviné de l'extension. | `getMimeType()` (`service/upload/upload-service.ts:153`) |
342
+ | `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts:49` |
343
+ | `moveAsync(target)` | Déplace le temp — **non bloquant, recommandé** dans le pipeline. | `moveAsync()` (`service/upload/upload-service.ts:193`) |
344
+ | `move(target)` | Variante **synchrone** (compat) — bloque l'event-loop. | `move()` (`service/upload/upload-service.ts:160`) |
345
+
346
+ `move`/`moveAsync` acceptent un **fichier cible** ou un **dossier existant** : sur un dossier, la
347
+ destination est bâtie avec `filename` — d'où l'avertissement de sécurité ci-dessous.
348
+
349
+ ### Gros upload sans pic mémoire — `@Body({ stream: true })`
350
+
351
+ Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
352
+ par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
353
+ `Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
354
+ (`routerDecorators.ts:1365`), mémoïsé sur la route.
355
+
356
+ ```ts
357
+ // fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
358
+ @Post("/backup")
359
+ async backup(@Body({ stream: true }) body: NodeJS.ReadableStream) {
360
+ await pipeline(body, createWriteStream("/var/app/backup.tar")); // node:stream/promises
361
+ return this.renderJson({ ok: true });
362
+ }
363
+ ```
364
+
365
+ ## 🔐 Sécurité
366
+
367
+ L'upload est une surface d'attaque classique : nom de fichier hostile, saturation disque/RAM, contenu
368
+ piégé. Les défenses en place, et **ce qui reste à ta charge**.
369
+
370
+ <!-- prettier-ignore -->
371
+ | Menace | Défense côté framework | À ta charge |
372
+ | --- | --- | --- |
373
+ | **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:594`). | La **destination** de `move()` (voir avertissement). |
374
+ | **Saturation RAM** | Multipart streamé (jamais bufferisé) ; corps non-multipart borné (`maxBodySize`). | Resserrer `maxBodySize` selon l'endpoint. |
375
+ | **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts:557`). | Purger les temp non déplacés (TTL / cron). |
376
+ | **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts:610`). | — |
377
+ | **Type de fichier hostile** | `mimeType` **déclaré** est exposé tel quel. | Valider le type/contenu réel (le MIME client est déclaratif). |
378
+
379
+ > [!WARNING]
380
+ > **Path traversal sur la destination.** `move(dir)` / `moveAsync(dir)` vers un **dossier** construit le
381
+ > chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts:197`). Un nom
382
+ > `../../etc/cron.d/x` s'échapperait du dossier. Le framework protège le chemin du **temporaire**, pas
383
+ > celui que **tu** choisis. Règle : passe une **cible complète** que tu contrôles, ou assainis toujours
384
+ > avec `path.basename(file.filename)` — exactement le `safeName` du Démarrage rapide.
385
+
386
+ ## ⚡ Performance & mémoire
387
+
388
+ Le parsing du corps est sur le chemin de chaque requête écrivante — les choix visibles dans le code :
389
+
390
+ - **Multipart en flux pur** — plus de double-bufferisation : busboy écrit sur disque au fil de l'eau,
391
+ seuls les champs texte restent en mémoire (`context/http/Request.ts:400`).
392
+ - **Rejet AVANT lecture** — le pré-check `Content-Length` renvoie `413` sans lire un octet
393
+ (`context/http/Request.ts:400`) ; le compteur streaming **libère immédiatement** la RAM déjà
394
+ bufferisée au dépassement (`context/http/parser.ts:43`).
395
+ - **`stat` non bloquant** — `UploadedFile.create()` résout les stats du fichier en async
396
+ (`service/upload/upload-service.ts:130`), plus de `lstatSync` par fichier uploadé.
397
+ - **Listeners jumeaux nettoyés** — le drain de fin de corps retire ses écouteurs `end`/`error`/overflow
398
+ à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts:88`).
399
+ - **Hash opt-in** — `hashAlgorithm: false` par défaut : zéro coût CPU tant que l'intégrité n'est pas
400
+ demandée.
401
+
402
+ Rejouer une charge d'upload : skill `nodefony-load-test`. Gate mémoire avant tout commit touchant le
403
+ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
404
+
405
+ ## 📜 Normes appliquées
406
+
407
+ | Domaine | Norme | Ancrage |
408
+ | ---------------------------------- | -------------------------------- | ------------------------------------------------------------- |
409
+ | Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:499`) |
410
+ | Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:407`) |
411
+ | 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts:33`) |
412
+ | Bornes multipart → 413 | RFC 9110 §15.5.14 | `stream.on("limit")` (`context/http/Request.ts:481`) |
413
+ | Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts:594`) |
414
+ | Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:799`) |
415
+
416
+ ## ⚠️ Pièges
417
+
418
+ | Symptôme | Cause | Correction |
419
+ | ------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
420
+ | `@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 |
421
+ | Un `POST` JSON de 1,5 Mo est refusé en `413` | `maxBodySize` vaut **1 MiB** par défaut | Augmenter `maxBodySize` (pas `upload.maxFileSize`) |
422
+ | Un gros fichier passe alors que `maxBodySize` est petit | Le multipart **n'écoute pas** `maxBodySize` | Régler `upload.maxFileSize` / `maxTotalFileSize` |
423
+ | `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`) |
424
+ | 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)` |
425
+ | 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) |
426
+ | `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:63`) |
427
+ | Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:799`) |
428
+ | `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:502`) |
429
+
430
+ ## 🧪 Tests & couverture
431
+
432
+ Les chiffres exacts vivent dans la carte de tests de cette page (régénérée depuis vitest, jamais figés
433
+ dans le Markdown).
434
+
435
+ | Type | Où |
436
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
437
+ | Unitaires — `UploadedFile` | `unit/UploadedFile.test.ts` — `create()` async, `move`/`moveAsync`, `mimeType`, `realName` |
438
+ | Unitaires — `@Body` stream | `framework` `unit/BodyStream.test.ts` — `@Body({ stream })`, `resolveParamArg`, `routeExpectsBodyStream` |
439
+ | Intégration — multipart | `http/upload.test.ts` — upload simple, multi-fichiers, mime non-octet, champ + fichier, `413` par fichier et cumulé |
440
+ | Intégration — content-types | `http/body-content-types.test.ts` — JSON, urlencoded, `@Body("champ")`, XML → `queryPost` |
441
+ | Intégration — bornes | `http/body-limit.test.ts` — sous la limite `200`, `Content-Length` > 1 MiB → `413`, chunked > 1 MiB refusé |
442
+ | Intégration — flux brut | `integration/bodyStream.test.ts` — `@Body({ stream })` bout-en-bout (TLS + clair), non-régression `@Body()` |
443
+
444
+ Ce qui **manque** aujourd'hui : pas de banc de **charge** dédié à l'upload (le streaming multipart n'est
445
+ exercé que fonctionnellement), pas de test **d'attaque** (`*.attack.test.ts`) sur le path traversal de
446
+ `move()` dans un dossier, et les bornes `maxFiles` / `maxFields` / `maxFieldsSize` ne sont pas couvertes
447
+ individuellement (seules `maxFileSize` et `maxTotalFileSize` le sont).
448
+
449
+ Suites : `npm test` (unitaires), `npm run test:integration` (serveur requis). Couverture :
450
+ `npm run coverage` dans `@nodefony/http` — le pourcentage vit dans le rapport vitest, jamais figé ici.
451
+ Skills associés : `nodefony-load-test`, `nodefony-check-memory-health`, `nodefony-security-review`.
452
+
453
+ ## 🔗 Pour aller plus loin
454
+
455
+ - ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
456
+ - 🧭 **Pages sœurs** : [Serveurs](servers.md) · [Sessions](session.md)
457
+ - Bornes de payload globales et transport → [Serveurs § Bornes de payload](servers.md#bornes-de-payload)
458
+ - Le pipeline qui reçoit ce corps parsé → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
459
+ - Routage, contrôleurs et décorateurs `@Body`/`@UploadedFiles` → [@nodefony/framework](../../framework/docs/index.md)
460
+ - Configuration d'application (`defineConfig`, `use`) → [configuration](../../../../../docs/guides/configuration.md)
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@nodefony/http",
3
+ "version": "10.0.0-alpha.1",
4
+ "description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
5
+ "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
+ "main": "./dist/index.js",
7
+ "type": "module",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
18
+ "dev": "rolldown -c rolldown.config.ts --watch",
19
+ "clean": "rimraf dist",
20
+ "test": "vitest run",
21
+ "test:integration": "vitest run --config vitest.integration.config.ts",
22
+ "test:load": "vitest run --config vitest.load.config.ts",
23
+ "test:memory": "vitest run --config vitest.load.config.ts nodefony/tests/http/memory.test.ts",
24
+ "coverage": "vitest run --coverage",
25
+ "typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
26
+ },
27
+ "private": false,
28
+ "engines": {
29
+ "node": ">=24.0.0"
30
+ },
31
+ "keywords": [
32
+ "nodefony",
33
+ "http",
34
+ "http2",
35
+ "websocket",
36
+ "https",
37
+ "tls",
38
+ "session",
39
+ "typescript",
40
+ "esm",
41
+ "nodejs"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/nodefony/nodefony-core.git",
46
+ "directory": "src/packages/@nodefony/http"
47
+ },
48
+ "directories": {
49
+ "lib": "./nodefony",
50
+ "test": "./nodefony/tests"
51
+ },
52
+ "dependencies": {
53
+ "@fastify/busboy": "3.2.2",
54
+ "cookie": "2.0.1",
55
+ "http-terminator": "3.2.0",
56
+ "mime-types": "3.0.2",
57
+ "ms": "^2.1.3",
58
+ "node-forge": "1.4.0",
59
+ "qs": "^6.15.3",
60
+ "serve-static": "2.2.1",
61
+ "tslib": "2.8.1",
62
+ "ws": "8.21.3",
63
+ "xml2js": "0.6.2"
64
+ },
65
+ "devDependencies": {
66
+ "@types/chai": "5.2.3",
67
+ "@types/mime-types": "3.0.1",
68
+ "@types/ms": "2.1.0",
69
+ "@types/node": "26.4.1",
70
+ "@types/node-forge": "1.3.14",
71
+ "@types/qs": "6.15.1",
72
+ "@types/serve-static": "2.2.0",
73
+ "@types/websocket": "1.0.10",
74
+ "@types/ws": "8.18.1",
75
+ "@types/xml2js": "0.4.14",
76
+ "@vitest/coverage-v8": "5.0.0",
77
+ "chai": "6.2.2",
78
+ "nodefony": "*",
79
+ "rimraf": "6.1.3",
80
+ "tsx": "4.23.13",
81
+ "vitest": "5.0.0"
82
+ },
83
+ "license": "CECILL-B",
84
+ "readmeFilename": "README.md",
85
+ "contributors": [],
86
+ "peerDependencies": {
87
+ "nodefony": "*",
88
+ "zod": "^4.4.3"
89
+ },
90
+ "files": [
91
+ "dist",
92
+ "docs"
93
+ ],
94
+ "publishConfig": {
95
+ "access": "public"
96
+ },
97
+ "homepage": "https://nodefony.github.io/nodefony-core/",
98
+ "bugs": {
99
+ "url": "https://github.com/nodefony/nodefony-core/issues"
100
+ }
101
+ }