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

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 (76) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +11 -5
  5. package/dist/nodefony/config/config.js +44 -4
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  7. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  8. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  9. package/dist/nodefony/service/audit-logger.js +2 -3
  10. package/dist/nodefony/service/certificates.js +140 -15
  11. package/dist/nodefony/service/error-renderer.js +1 -1
  12. package/dist/nodefony/service/http-kernel.js +221 -47
  13. package/dist/nodefony/service/reverse-proxy.js +336 -0
  14. package/dist/nodefony/service/servers/server-http.js +3 -3
  15. package/dist/nodefony/service/servers/server-https.js +3 -3
  16. package/dist/nodefony/service/servers/server-static.js +4 -7
  17. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  18. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  19. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  20. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  21. package/dist/nodefony/service/upload/upload-service.js +3 -3
  22. package/dist/nodefony/src/context/Context.js +28 -1
  23. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  24. package/dist/nodefony/src/context/forwarded.js +2 -2
  25. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  26. package/dist/nodefony/src/context/http/Request.js +1 -9
  27. package/dist/nodefony/src/context/http/Response.js +16 -1
  28. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  29. package/dist/nodefony/src/context/responseEnded.js +27 -0
  30. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  31. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  32. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  33. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  34. package/dist/nodefony/src/proxy/forward.js +476 -0
  35. package/dist/nodefony/src/proxy/rules.js +182 -0
  36. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  37. package/dist/nodefony/src/session/session.js +28 -12
  38. package/dist/types/index.d.ts +5 -0
  39. package/dist/types/nodefony/config/config.d.ts +16 -0
  40. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  41. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  42. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  43. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  44. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  45. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  46. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  47. package/dist/types/nodefony/service/certificates.d.ts +96 -5
  48. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  49. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  50. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  51. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  52. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  53. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  54. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  55. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  56. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  57. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  58. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  59. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  60. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  61. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  62. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  63. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  64. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  65. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  66. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  67. package/docs/cookies.md +13 -13
  68. package/docs/index.md +5 -1
  69. package/docs/observabilite.md +15 -15
  70. package/docs/rate-limit.md +30 -30
  71. package/docs/reverse-proxy.md +522 -0
  72. package/docs/servers.md +94 -60
  73. package/docs/session.md +61 -61
  74. package/docs/sse.md +283 -0
  75. package/docs/upload.md +23 -23
  76. package/package.json +8 -8
package/docs/upload.md CHANGED
@@ -62,10 +62,10 @@ flowchart TD
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:513`)
65
+ 1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:502`)
66
66
  aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / parser brut selon l'en-tête.
67
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:585`) — jamais bufferisés en RAM.
68
+ fichier temporaire (`parseMultipart()`, `context/http/Request.ts:574`) — jamais bufferisés en RAM.
69
69
  JSON / urlencoded / XML restent en mémoire (petits corps).
70
70
  3. **Deux budgets distincts, jamais confondus.** Le corps **non-multipart** est borné par `maxBodySize`
71
71
  (défaut **1 MiB**) ; le multipart a **ses propres** limites busboy (`maxFileSize`, `maxTotalFileSize`,
@@ -117,16 +117,16 @@ Trois choix structurent l'implémentation, et chacun a une raison de sécurité
117
117
 
118
118
  **Le multipart ne touche jamais la RAM.** Là où l'ancien chemin bufferisait le corps entier, busboy lit
119
119
  le flux et écrit chaque fichier au fil de l'eau dans le dossier temporaire (`streamMultipart()`,
120
- `context/http/Request.ts:626`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
120
+ `context/http/Request.ts:615`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
121
121
  champs texte restent en mémoire. C'est ce qui rend un endpoint d'upload public tenable.
122
122
 
123
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:683`) : un nom malveillant
124
+ `randomUUID()` + extension d'origine (`context/http/Request.ts:672`) : un nom malveillant
125
125
  (`../../etc/passwd`) ne peut pas influencer le **chemin** d'écriture. Le nom d'origine est conservé en
126
126
  **métadonnée** (`filename`), pas dans le chemin.
127
127
 
128
128
  **Deux budgets, secure-by-default.** Le corps non-multipart est plafonné à **1 MiB** par défaut
129
- (`maxBodySize`, `http/nodefony/config/config.ts:1027`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
129
+ (`maxBodySize`, `http/nodefony/config/config.ts:1208`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
130
130
  multipart, lui, a ses propres bornes busboy (par fichier, cumul, nombre) qui coupent le flux et
131
131
  nettoient les temporaires déjà posés au moindre dépassement (`context/http/Request.ts:481`).
132
132
 
@@ -257,10 +257,10 @@ sequenceDiagram
257
257
  Les points d'implémentation qui expliquent des comportements surprenants :
258
258
 
259
259
  1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
260
- (`context/http/Request.ts:513`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
260
+ (`context/http/Request.ts:502`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
261
261
  des méthodes parsées (`context/http/Request.ts:94`) — l'oubli laissait tout `PATCH` avec un corps vide.
262
262
  2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
263
- (`context/http/Request.ts:626`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
263
+ (`context/http/Request.ts:615`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
264
264
  qu'une fois tous les fichiers fermés (`bb.on("finish")`, `context/http/Request.ts:707`).
265
265
  3. **Une limite dépassée nettoie les temporaires déjà posés** — `abort()`
266
266
  (`context/http/Request.ts:646`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
@@ -283,10 +283,10 @@ non-multipart n'écoute pas `upload.*`.
283
283
  | `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
284
284
 
285
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:490`), puis un **compteur
286
+ qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:479`), puis un **compteur
287
287
  en streaming** qui coupe le socket si le corps déborde sans `Content-Length` honnête — chunked ou
288
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:1027`.
289
+ Champ `maxBodySize` du schéma : `config/config.ts:1208`.
290
290
 
291
291
  **Par requête** : `maxBodySize` et les limites `upload.*` (hors `uploadDir`) sont sur la liste blanche
292
292
  du calque de configuration — une application peut les changer pour une requête
@@ -322,16 +322,16 @@ recommandé) et les **getters** de `Controller` (impératif). Les signatures exa
322
322
 
323
323
  | Accès | Source lue | Ancrage |
324
324
  | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
325
- | `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1283`) |
326
- | `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1282`) |
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
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:1223`) |
329
- | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1326`) |
330
- | `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:341`) |
331
- | `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:351`) |
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`) |
332
332
 
333
333
  Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
334
- (`routerDecorators.ts:1287`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
334
+ (`routerDecorators.ts:1312`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
335
335
  `IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
336
336
 
337
337
  ### Un fichier uploadé — `UploadedFile`
@@ -357,7 +357,7 @@ destination est bâtie avec `filename` — d'où l'avertissement de sécurité c
357
357
  Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
358
358
  par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
359
359
  `Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
360
- (`routerDecorators.ts:1408`), mémoïsé sur la route.
360
+ (`routerDecorators.ts:1509`), mémoïsé sur la route.
361
361
 
362
362
  ```ts
363
363
  // fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
@@ -376,7 +376,7 @@ piégé. Les défenses en place, et **ce qui reste à ta charge**.
376
376
  <!-- prettier-ignore -->
377
377
  | Menace | Défense côté framework | À ta charge |
378
378
  | --- | --- | --- |
379
- | **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:683`). | La **destination** de `move()` (voir avertissement). |
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). |
380
380
  | **Saturation RAM** | Multipart streamé (jamais bufferisé) ; corps non-multipart borné (`maxBodySize`). | Resserrer `maxBodySize` selon l'endpoint. |
381
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
382
  | **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts:754`). | — |
@@ -412,12 +412,12 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
412
412
 
413
413
  | Domaine | Norme | Ancrage |
414
414
  | ---------------------------------- | -------------------------------- | ------------------------------------------------------------- |
415
- | Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:585`) |
416
- | Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:490`) |
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
417
  | 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts:33`) |
418
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:683`) |
420
- | Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:886`) |
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`) |
421
421
 
422
422
  ## ⚠️ Pièges
423
423
 
@@ -430,7 +430,7 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
430
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
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
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:886`) |
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
434
  | `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:521`) |
435
435
 
436
436
  ## 🧪 Tests & couverture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/http",
3
- "version": "10.0.0-beta.2",
3
+ "version": "10.0.0-beta.3",
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",
@@ -51,7 +51,7 @@
51
51
  "test": "./nodefony/tests"
52
52
  },
53
53
  "dependencies": {
54
- "@fastify/busboy": "3.2.2",
54
+ "@fastify/busboy": "3.2.3",
55
55
  "cookie": "2.0.1",
56
56
  "http-terminator": "3.2.0",
57
57
  "mime-types": "3.0.2",
@@ -66,23 +66,23 @@
66
66
  "@types/chai": "5.2.3",
67
67
  "@types/mime-types": "3.0.1",
68
68
  "@types/ms": "2.1.0",
69
- "@types/node": "26.6.3",
69
+ "@types/node": "26.6.5",
70
70
  "@types/qs": "6.15.1",
71
71
  "@types/serve-static": "2.2.0",
72
72
  "@types/websocket": "1.0.10",
73
73
  "@types/ws": "8.18.2",
74
74
  "@types/xml2js": "0.4.14",
75
- "@vitest/coverage-v8": "5.0.3",
76
- "nodefony": "^10.0.0-beta.2",
75
+ "@vitest/coverage-v8": "5.0.4",
76
+ "nodefony": "^10.0.0-beta.3",
77
77
  "tsx": "4.23.15",
78
- "vitest": "5.0.3"
78
+ "vitest": "5.0.4"
79
79
  },
80
80
  "license": "Apache-2.0",
81
81
  "readmeFilename": "README.md",
82
82
  "contributors": [],
83
83
  "peerDependencies": {
84
- "nodefony": "^10.0.0-beta.2",
85
- "zod": "^4.6.5"
84
+ "nodefony": "^10.0.0-beta.3",
85
+ "zod": "^4.6.7"
86
86
  },
87
87
  "files": [
88
88
  "dist",