@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
@@ -0,0 +1,935 @@
1
+ ---
2
+ title: "Serveurs — HTTP, HTTPS, HTTP/2, WebSocket"
3
+ navTitle: Serveurs
4
+ lang: fr
5
+ module: "@nodefony/http"
6
+ topic: servers
7
+ section: "Cœur runtime"
8
+ audience: [developer, devops]
9
+ tags:
10
+ [
11
+ serveurs,
12
+ http,
13
+ https,
14
+ http2,
15
+ websocket,
16
+ wss,
17
+ tls,
18
+ certificats,
19
+ ports,
20
+ health,
21
+ shutdown,
22
+ ]
23
+ version: "doc"
24
+ status: stable
25
+ updated: 2026-07-19
26
+ source: "src/packages/@nodefony/http/docs/servers.md"
27
+ coverageModule: http
28
+ coverageFiles: servers/server-http.ts,servers/server-https.ts,servers/server-websocket.ts,certificates.ts
29
+ ---
30
+
31
+ # Serveurs — HTTP, HTTPS, HTTP/2, WebSocket
32
+
33
+ > Ce sont les portes d'entrée du processus : ce qui ouvre les sockets, accepte les connexions et
34
+ > transmet chaque requête — web ou temps réel — au **même** pipeline. Une application Nodefony ouvre
35
+ > jusqu'à **deux ports** (5151 en clair, 5152 en TLS) et y adosse **quatre serveurs** (HTTP, HTTPS/HTTP-2,
36
+ > WS, WSS). Cette page décrit ce qu'ils écoutent, comment on les règle, comment ils démarrent, comment
37
+ > ils s'arrêtent, et pourquoi ils ne doivent jamais tomber. Chaque fait est ancré sur le code.
38
+
39
+ 📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Serveurs**
40
+
41
+ ## 🧠 Le modèle mental — deux ports, quatre serveurs, un pipeline
42
+
43
+ Un serveur Nodefony n'est pas un « framework qui écoute ». C'est un **assemblage de services** : chaque
44
+ serveur est un service injectable qui possède son socket, et tous délèguent au même orchestrateur,
45
+ `HttpKernel`. Le WebSocket **n'a pas de port à lui** : il se greffe sur le serveur HTTP correspondant.
46
+
47
+ ```mermaid
48
+ flowchart TD
49
+ CFG["nodefony.config.ts<br/>servers: { http, https }"] --> INIT["HttpKernel.initServers()"]
50
+ INIT -->|"servers.http ≠ false"| SH["server-http<br/>node:http — 5151"]
51
+ INIT -->|"servers.https ≠ false"| SS["server-https<br/>node:https ou node:http2 — 5152"]
52
+ SH --> WS["server-websocket<br/>ws:// sur 5151"]
53
+ SS --> WSS["server-websocket-secure<br/>wss:// sur 5152"]
54
+ SH --> HK["HttpKernel<br/>onHttpRequest / onWebsocketRequest"]
55
+ SS --> HK
56
+ WS --> HK
57
+ WSS --> HK
58
+ HK --> PIPE["pipeline commun<br/>contexte → routing → firewall → contrôleur"]
59
+ ```
60
+
61
+ Trois idées à retenir, et tout le reste en découle :
62
+
63
+ 1. **Un serveur = un service DI.** Ils sont déclarés dans `@services([…])` du module
64
+ (`src/packages/@nodefony/http/index.ts:52`) et instanciés par le conteneur, comme n'importe quel
65
+ service — donc introspectables, testables, remplaçables.
66
+ 2. **Le WS hérite de son porteur.** `server-websocket` reçoit le `http.Server` déjà en écoute
67
+ (`Websocket.createServer()`, `server-websocket.ts:62`) ; `server-websocket-secure` reçoit le
68
+ `https.Server`. Couper HTTPS coupe donc le WSS, sans autre réglage.
69
+ 3. **Les quatre convergent vers `HttpKernel`.** Les serveurs ne connaissent ni les routes, ni les
70
+ sessions, ni le firewall : ils branchent `onHttpRequest` / `onWebsocketRequest` et se taisent.
71
+
72
+ ## 📖 Lexique
73
+
74
+ | Terme | Sens |
75
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------- |
76
+ | Bind / `listen` | Réserver un port auprès du noyau. Opération **atomique** : elle réussit, ou le noyau répond `EADDRINUSE`. |
77
+ | `EADDRINUSE` | Code d'erreur système « adresse déjà utilisée » — un autre processus occupe le port. |
78
+ | ALPN | _Application-Layer Protocol Negotiation_ : le client et le serveur choisissent `h2` ou `http/1.1` pendant le TLS. |
79
+ | h2 / HTTP-2 | Version multiplexée de HTTP : plusieurs flux (streams) dans une seule connexion TCP. |
80
+ | `allowHTTP1` | Option Node qui autorise un serveur HTTP/2 à servir aussi les clients HTTP/1.1 restés en arrière. |
81
+ | Upgrade | Requête HTTP `GET` + en-tête `Upgrade: websocket` qui bascule la connexion en WebSocket (réponse `101`). |
82
+ | Frame | Unité de message WebSocket (RFC 6455). |
83
+ | Zombie (half-open) | Socket dont le pair a disparu sans fermer proprement : TCP paraît ouvert, personne n'est en face. |
84
+ | Ping / Pong | Frames de contrôle WebSocket servant de battement de cœur (keep-alive). |
85
+ | Drain | Vidange : laisser finir les requêtes en cours avant de fermer, plutôt que couper les sockets. |
86
+ | Liveness / Readiness | Sondes cloud : « le processus est-il vivant ? » / « peut-il recevoir du trafic ? ». |
87
+ | SIGTERM | Signal d'arrêt poli envoyé par l'orchestrateur (k8s, `docker stop`) avant le `SIGKILL`. |
88
+ | SAN | _Subject Alternative Name_ : la liste des noms/IP qu'un certificat couvre réellement (RFC 6125). |
89
+ | mkcert | Outil de développement qui installe une autorité de certification locale **de confiance** sur la machine. |
90
+ | Reverse-proxy / edge | Le nginx / HAProxy / ingress placé devant l'application, souvent porteur du TLS. |
91
+ | `X-Forwarded-*` | En-têtes ajoutés par un proxy pour dire l'IP, l'hôte et le protocole d'origine du client. |
92
+ | CSWSH | _Cross-Site WebSocket Hijacking_ : une page tierce ouvre un WebSocket authentifié par le cookie de la victime. |
93
+ | `SO_REUSEPORT` | Option système permettant à N processus d'écouter le **même** port, le noyau répartissant les connexions. |
94
+
95
+ ## Qu'est-ce qu'un serveur, ici ?
96
+
97
+ Imagine un immeuble avec **deux entrées** : une porte de service (HTTP en clair, 5151) et une porte
98
+ principale sécurisée (TLS, 5152). Derrière chaque porte, un **hall unique** : peu importe par où l'on
99
+ entre, on aboutit au même accueil, qui oriente vers le bon bureau. Le WebSocket, lui, n'est pas une
100
+ troisième porte : c'est un **interphone installé sur les portes existantes** — il emprunte la même
101
+ serrure, la même adresse, la même politique de sécurité.
102
+
103
+ Concrètement, un serveur Nodefony a quatre responsabilités, et rien d'autre :
104
+
105
+ 1. **Ouvrir** — réserver un port, ou en négocier un autre si celui-ci est pris.
106
+ 2. **Régler le transport** — délais, taille des en-têtes, TLS, compression WebSocket, limites HTTP/2.
107
+ 3. **Transmettre** — passer chaque requête / connexion au `HttpKernel`, sans jamais l'interpréter.
108
+ 4. **Fermer proprement** — vider les requêtes en cours, prévenir les clients WebSocket, libérer le port.
109
+
110
+ Tout ce qui suit (routing, session, firewall, contrôleur) appartient au pipeline, décrit dans
111
+ [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md).
112
+
113
+ ## La vision Nodefony
114
+
115
+ Le différenciateur du framework — **HTTP et WebSocket dans le même contexte de contrôleur** — se joue
116
+ ici, au niveau du transport. Trois choix structurent l'implémentation.
117
+
118
+ **Node natif, rien d'autre.** `node:http`, `node:https`, `node:http2` et la bibliothèque `ws`. Pas
119
+ d'abstraction de serveur maison, pas de runtime alternatif. Ce que Node sait faire, Nodefony le laisse
120
+ faire — il n'ajoute que ce que Node ne fournit pas : la politique de port, le drain, le keep-alive WS,
121
+ les probes.
122
+
123
+ **Un port TLS qui parle deux protocoles.** Quand `servers.https.protocol` vaut `"2.0"` (le défaut),
124
+ Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttps.createServerH2()`,
125
+ `server-https.ts:174`) : les clients modernes négocient `h2` par ALPN, les autres restent en HTTP/1.1
126
+ **sur le même port**. Le protocole effectif est relu socket par socket pour taguer le contexte
127
+ (`server-https.ts:222`).
128
+
129
+ **Le WebSocket n'est jamais un citoyen de seconde zone.** Il est adossé au serveur HTTP porteur
130
+ (`server-websocket.ts:80`), passe par le **même** rate-limit d'IP que les requêtes HTTP — un upgrade
131
+ _est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1497`) —, hérite de la même
132
+ session et du même firewall, et se ferme avec le même soin qu'une réponse HTTP.
133
+
134
+ > [!NOTE]
135
+ > Le serveur HTTP/3 (QUIC) est **réservé, pas implémenté** : la clé `http3` existe dans le schéma,
136
+ > marquée `reserved` (`config.ts:1029`). Elle ne fait rien aujourd'hui.
137
+
138
+ ## 🚀 Démarrage rapide
139
+
140
+ Dans une application générée par `nodefony create app`, **les serveurs sont déjà là** : le manifeste
141
+ charge `@nodefony/http`, et le kernel ouvre 5151 et 5152 au boot. On n'écrit que ses **écarts**.
142
+
143
+ ### 1. La topologie — quels serveurs, sur quels ports
144
+
145
+ La topologie appartient à l'**application** (bloc `servers`), pas au module : c'est une propriété du
146
+ déploiement.
147
+
148
+ ```typescript
149
+ // nodefony.config.ts — la topologie réseau de l'app
150
+ export default defineConfig((ctx) => ({
151
+ // Un conteneur doit écouter TOUTES les interfaces : un bind 127.0.0.1 n'est
152
+ // jamais atteint par le port mapping Docker/k8s.
153
+ domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1",
154
+ servers: {
155
+ http: { port: 5151 },
156
+ // protocol "2.0" = HTTP/2 (h2) avec repli HTTP/1.1 sur le MÊME port.
157
+ https: { port: 5152, protocol: "2.0" },
158
+ // Port occupé : en dev on glisse au suivant (annoncé) ; en prod on échoue.
159
+ portPolicy: ctx.isProd ? "strict" : "auto",
160
+ },
161
+ modules: ["@nodefony/http", "@nodefony/framework"],
162
+ }));
163
+ ```
164
+
165
+ ### 2. Le réglage — le comportement de chaque serveur
166
+
167
+ Le réglage appartient au **module**, colocalisé dans le manifeste via `use()`. Toutes les clés
168
+ ci-dessous sont facultatives : ce sont les défauts du schéma Zod, écrits ici pour les montrer.
169
+
170
+ ```typescript
171
+ // nodefony.config.ts — réglage transport (extrait du manifeste `modules`)
172
+ export default defineConfig(() => ({
173
+ modules: [
174
+ use("@nodefony/http", {
175
+ // Ne pas exposer l'identité du serveur (anti-empreinte, recommandé en prod).
176
+ headerServer: null,
177
+ // Barrière Host : le domaine canonique est toujours accepté, + ceux-ci.
178
+ trustedHosts: ["app.example.com"],
179
+ // Un seul reverse-proxy en amont ? alors et alors seulement, lui faire confiance.
180
+ trustProxy: false,
181
+ http: {
182
+ requestTimeout: 30_000, // anti slow-loris (réception complète)
183
+ keepAliveTimeout: 5_000, // réutilisation de la socket TCP
184
+ shutdownTimeout: 5_000, // drain avant destruction forcée
185
+ },
186
+ http2: {
187
+ maxConcurrentStreams: 100, // défense CVE-2023-44487 (Rapid Reset)
188
+ maxSessionMemory: 10, // Mo par session h2
189
+ },
190
+ websocket: {
191
+ maxPayload: 1024 * 1024, // 1 MiB → au-delà, close 1009
192
+ keepaliveInterval: 20_000, // ping toutes les 20 s
193
+ keepaliveGracePeriod: 10_000, // pong attendu sous 10 s, sinon zombie
194
+ },
195
+ // Probes cloud-native — actives par défaut.
196
+ health: {
197
+ enabled: true,
198
+ livenessPath: "/livez",
199
+ readinessPath: "/readyz",
200
+ },
201
+ certificates: { strategy: "auto" }, // mkcert en dev, sinon auto-signé
202
+ }),
203
+ "@nodefony/framework",
204
+ ],
205
+ }));
206
+ ```
207
+
208
+ ### 3. Le contrôleur qui répond sur les deux transports
209
+
210
+ Rien de spécifique aux serveurs : c'est justement le propos. Le même contrôleur sert le web et le
211
+ temps réel, et lit le transport dans son contexte.
212
+
213
+ ```typescript
214
+ // nodefony/controller/PingController.ts — complet, compile tel quel
215
+ import { Controller, controller, Get, route } from "@nodefony/framework";
216
+ import type { Context } from "@nodefony/http";
217
+
218
+ @controller("/ping")
219
+ class PingController extends Controller {
220
+ constructor(context: Context) {
221
+ super("PingController", context);
222
+ }
223
+
224
+ // HTTP : répond sur http://…:5151/ping ET https://…:5152/ping (h2 inclus).
225
+ @Get("/")
226
+ async ping() {
227
+ return this.renderJson({
228
+ scheme: this.context?.scheme, // "http" | "https"
229
+ type: this.context?.type, // "http" | "http2" | "websocket"
230
+ });
231
+ }
232
+
233
+ // WebSocket : ws://…:5151/ping/live et wss://…:5152/ping/live.
234
+ // `message == null` = handshake (la connexion vient de s'ouvrir).
235
+ @route("ping-live", {
236
+ path: "/live",
237
+ requirements: { methods: ["WEBSOCKET"], protocol: "" },
238
+ })
239
+ async live(message: string | Buffer | null) {
240
+ if (message == null) {
241
+ return this.renderJson({ handshake: true });
242
+ }
243
+ return this.render(message.toString());
244
+ }
245
+ }
246
+
247
+ export default PingController;
248
+ ```
249
+
250
+ ### 4. Ce qu'on observe au boot
251
+
252
+ Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1090`), puis affiche les URL réellement
253
+ en écoute — le récap de développement liste HTTP, HTTP/2, WS et WSS dans cet ordre
254
+ (`BootReporter.ts:389`) :
255
+
256
+ ```text
257
+ ✓ Prêt en 1.4s
258
+
259
+ Serveurs
260
+ ➜ HTTP http://127.0.0.1:5151
261
+ ➜ HTTP/2 https://127.0.0.1:5152
262
+ ➜ WS ws://127.0.0.1:5151
263
+ ➜ WSS wss://127.0.0.1:5152
264
+ ```
265
+
266
+ Hors écran animé (production, CI, `--debug`), ce sont les bannières par serveur qui sortent
267
+ (`ServerHttp.showBanner()`, `server-http.ts:226`, appelées par le kernel — `Kernel.ts:457`) :
268
+
269
+ ```text
270
+ Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
271
+ ```
272
+
273
+ ### 5. Vérifier depuis le terminal
274
+
275
+ ```bash
276
+ # HTTP/1.1 en clair
277
+ curl -s http://127.0.0.1:5151/ping
278
+ # {"scheme":"http","type":"http"}
279
+
280
+ # HTTP/2 sur le port TLS (-k : le certificat de dev n'est pas dans le trust store)
281
+ curl -sk --http2 https://127.0.0.1:5152/ping
282
+ # {"scheme":"https","type":"http2"}
283
+
284
+ # Le même port TLS sert encore HTTP/1.1 (allowHTTP1) — repli négocié par ALPN
285
+ curl -sk --http1.1 https://127.0.0.1:5152/ping
286
+ # {"scheme":"https","type":"https"}
287
+
288
+ # Probes cloud-native, servies par les DEUX serveurs
289
+ curl -si http://127.0.0.1:5151/livez | head -1 # HTTP/1.1 200 OK
290
+ curl -sik https://127.0.0.1:5152/readyz | head -1 # HTTP/2 200
291
+ ```
292
+
293
+ ### Cas cloud-native — un seul port, TLS terminé à l'ingress
294
+
295
+ C'est le déploiement **nominal** en Kubernetes : l'ingress porte le certificat, le pod sert en clair.
296
+ `https: false` désactive HTTPS **et** le WSS qui en hérite, et supprime au passage toute génération de
297
+ certificat au boot.
298
+
299
+ ```typescript
300
+ // nodefony.config.ts — pod k8s derrière un ingress qui termine le TLS
301
+ export default defineConfig((ctx) => ({
302
+ domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1",
303
+ servers: {
304
+ https: false, // un seul port exposé : 5151
305
+ },
306
+ modules: [
307
+ use("@nodefony/http", {
308
+ // L'ingress est l'unique point d'entrée → ses X-Forwarded-* sont fiables.
309
+ trustProxy: ctx.isProd ? "uniquelocal" : false,
310
+ // L'ingress filtre déjà le Host ; sinon lister les vhosts servis.
311
+ trustedHosts: ["app.example.com"],
312
+ }),
313
+ "@nodefony/framework",
314
+ ],
315
+ }));
316
+ ```
317
+
318
+ ## 🔌 Les quatre serveurs (et le cinquième service)
319
+
320
+ Choisir en cinq secondes :
321
+
322
+ | Service | Écoute | Créé si… | Rôle |
323
+ | ------------------------- | ------------------- | ------------------------- | -------------------------------------------------- |
324
+ | `server-http` | `http://` — 5151 | `servers.http !== false` | HTTP/1.1 en clair. |
325
+ | `server-https` | `https://` — 5152 | `servers.https !== false` | TLS : HTTP/2 (défaut) ou HTTP/1.1. |
326
+ | `server-websocket` | `ws://` — 5151 | `server-http` actif | WebSocket adossé au serveur HTTP. |
327
+ | `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
328
+ | `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
329
+
330
+ L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1033`) : chaque serveur est
331
+ consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:235`).
332
+ Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
333
+
334
+ ### `server-http` — HTTP/1.1 en clair
335
+
336
+ Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.createServer()`
337
+ (`server-http.ts:68`) crée le `http.Server`, applique les réglages de transport, branche
338
+ `onHttpRequest`, puis écoute selon la politique de port.
339
+
340
+ - **Actif** si `servers.http` n'est pas `false` — la décision est prise dès le constructeur
341
+ (`server-http.ts:56`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
342
+ `server-http.ts:61`).
343
+ - **Réglages appliqués** : `requestTimeout` (anti slow-loris), `maxHeadersCount`, `timeout` de socket
344
+ (qui émet un événement `onTimeout`), `keepAliveTimeout`.
345
+ - **Erreurs de protocole** : l'événement `clientError` est traité explicitement
346
+ (`server-http.ts:167`) — voir la section Résilience, c'est un piège Node à part entière.
347
+
348
+ ### `server-https` — TLS, et HTTP/2 par défaut
349
+
350
+ Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:88`) :
351
+
352
+ - **`"2.0"` (défaut)** → `http2.createSecureServer` avec `allowHTTP1: true`
353
+ (`ServerHttps.createServerH2()`, `server-https.ts:174`). Les bornes anti-DoS HTTP/2 ne sont posées
354
+ **que si elles sont configurées**, pour ne pas écraser les défauts de Node
355
+ (`maxSessionMemory`, `server-https.ts:197`).
356
+ Les erreurs de session et de flux sont journalisées sans tuer le serveur
357
+ (`sessionError`, `server-https.ts:274`).
358
+ - **`"1.1"`** → `https.createServer` classique.
359
+
360
+ Le certificat vient du service `certificates`, lu au moment de la création via
361
+ `serviceCerticats` (`server-https.ts:94`) — voir la section dédiée.
362
+
363
+ > [!IMPORTANT]
364
+ > Sur la branche HTTP/2, le protocole **effectif** est relu par requête via l'ALPN de la socket TLS
365
+ > (`server-https.ts:222`) : un client `h2` produit un contexte de type `http2`, un client HTTP/1.1 sur
366
+ > le même port produit un contexte `https`. C'est ce qui rend le port TLS universel.
367
+
368
+ ### `server-websocket` — WebSocket en clair
369
+
370
+ Il ne crée **aucun** socket : il reçoit le `http.Server` déjà en écoute et s'y greffe
371
+ (`server-websocket.ts:62`), puis relit `address()` pour connaître son port — il suit donc
372
+ automatiquement un éventuel décalage de port.
373
+
374
+ - **Options transmises à `ws`** telles quelles (compression, validation UTF-8, `maxPayload`…), avec deux
375
+ réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
376
+ le battement de cœur (`server-websocket.ts:80`).
377
+ - **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts:87`) et par connexion
378
+ (`trackPong()`, `server-websocket.ts:107`).
379
+ - **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
380
+ (`prependOnceListener`, `server-websocket.ts:91`) — l'ordre
381
+ compte, voir la section Arrêt gracieux.
382
+
383
+ ### `server-websocket-secure` — WebSocket sur TLS
384
+
385
+ Même code, une différence qui a son importance : il lit sa **propre** section de configuration,
386
+ `websocketSecure`, et non `websocket` (`server-websocket-secure.ts:50`). Régler `websocket.maxPayload`
387
+ ne change donc rien au WSS ; les deux sections ont la même forme et les mêmes défauts.
388
+
389
+ ### `server-static` — les fichiers, en repli
390
+
391
+ Pas de port : c'est un service greffé sur le pipeline. Depuis la bascule « router d'abord », il n'est
392
+ consulté qu'**après** un échec de routage — une requête qui matche une route ne touche plus le disque.
393
+ Il se désactive par `statics.enabled: false` (cas cloud-native : nginx ou un CDN sert les fichiers) sans
394
+ pour autant supprimer les montages programmatiques.
395
+
396
+ ## ⚙️ Configuration — deux niveaux, jamais mélangés
397
+
398
+ C'est la distinction la plus utile de cette page, et celle qu'on rate le plus souvent.
399
+
400
+ | Question | Où ça se règle | Source |
401
+ | ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
402
+ | **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts:132`) |
403
+ | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:947`) |
404
+
405
+ Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
406
+ la CI et le cluster) ; le **réglage** est une propriété de l'application.
407
+
408
+ ### Niveau 1 — la topologie (`servers`)
409
+
410
+ <!-- prettier-ignore -->
411
+ | Option | Type | Défaut | Effet |
412
+ | --- | --- | --- | --- |
413
+ | `servers.http` | `{ port }` \| `false` | `{ port: 5151 }` | Serveur en clair. `false` = TLS-only (le WS tombe avec). |
414
+ | `servers.https` | `{ port, protocol }` \| `false` | `{ port: 5152, protocol: "2.0" }` | Serveur TLS. `false` = nominal cloud-native (le WSS tombe). |
415
+ | `servers.https.protocol` | `"1.1"` \| `"2.0"` | `"2.0"` | HTTP/2 (h2) avec repli HTTP/1.1 par ALPN, ou HTTP/1.1 seul. |
416
+ | `servers.statics` | bool | `true` | Monte le service de fichiers statiques. |
417
+ | `servers.portPolicy` | `"auto"` \| `"strict"` | `auto` en dev, `strict` en prod **et** test | Que faire si le port est occupé (section suivante). |
418
+ | `servers.portRetryAttempts` | int ≥ 0 | `20` | En `auto`, nombre de ports essayés après le port désiré. |
419
+
420
+ Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaults.ts:34`).
421
+
422
+ ### Niveau 2 — le transport HTTP / HTTPS
423
+
424
+ Table dérivée de `httpServerSchema` (`config.ts:257`) ; la section `https` reprend les mêmes clés et en
425
+ ajoute une (`httpsServerSchema`, `config.ts:315`).
426
+
427
+ | Option | Type | Défaut | Effet |
428
+ | ---------------------------- | ----- | -------- | -------------------------------------------------------------------------------- |
429
+ | `maxHeadersCount` | int | `2000` | Nombre maximum d'en-têtes par requête — anti _header flooding_. |
430
+ | `keepAliveTimeout` | ms | `5000` | Délai de réutilisation de la socket TCP entre deux requêtes. |
431
+ | `timeout` | ms | `120000` | Timeout global de socket. `0` = désactivé. |
432
+ | `requestTimeout` | ms | `30000` | Délai de réception de la requête complète — **anti slow-loris**. |
433
+ | `responseTimeout` | ms | `30000` | Délai d'envoi de la réponse complète (couche pipeline). |
434
+ | `shutdownTimeout` | ms | `5000` | Drain au shutdown avant destruction forcée. Garder < grâce orchestrateur. |
435
+ | `headers` | objet | `null` | En-têtes ajoutés à toutes les réponses. |
436
+ | `rejectUnauthorized` (https) | bool | `false` | Rejette les certificats invalides. `false` en dev (auto-signés), `true` en prod. |
437
+
438
+ Ces deux sections sont **permissives** (`z.looseObject`) : toute option supplémentaire de
439
+ `http.Server` / `net.Server` / TLS (`insecureHTTPParser`, `ciphers`, `minVersion`…) est transmise telle
440
+ quelle à Node. C'est délibéré — un schéma strict effacerait silencieusement une option légitime.
441
+
442
+ ### Niveau 2 — HTTP/2
443
+
444
+ Depuis `http2Schema` (`config.ts:332`), appliqué seulement si défini
445
+ (`maxSessionMemory`, `server-https.ts:197`).
446
+
447
+ | Option | Type | Défaut | Effet |
448
+ | ---------------------- | ---- | ------ | ----------------------------------------------------------------------------- |
449
+ | `maxConcurrentStreams` | int | `100` | Flux concurrents par session h2 — **défense CVE-2023-44487** (_Rapid Reset_). |
450
+ | `maxSessionMemory` | Mo | `10` | Mémoire maximale par session h2 — borne l'amplification mémoire. |
451
+
452
+ ### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
453
+
454
+ Depuis `websocketSchema` (`config.ts:490`). Les deux sections partagent la forme et les défauts ; le WSS
455
+ lit `websocketSecure` (`config.ts:1037`).
456
+
457
+ | Option | Type | Défaut | Effet |
458
+ | ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------- |
459
+ | `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
460
+ | `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
461
+ | `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket. |
462
+ | `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts:516`). |
463
+ | `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts:525`). |
464
+ | `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
465
+ | `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
466
+ | `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
467
+ | `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité). |
468
+ | `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM. |
469
+ | `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013). |
470
+
471
+ > [!WARNING]
472
+ > `keepalive*`, `closeTimeout`, `maxBackpressure` et `backpressurePolicy` sont des **réglages
473
+ > Nodefony**, pas des options de `ws` : la bibliothèque les ignore, c'est le framework qui les
474
+ > implémente. À l'inverse, `server`, `noServer`, `clientTracking`, `host`, `port` et `backlog` sont
475
+ > **gérés par Nodefony** et non exposés — les forcer n'aurait pas d'effet.
476
+
477
+ ## ⚙️ Ports occupés — la politique de repli
478
+
479
+ ### La situation
480
+
481
+ Tu développes sur le framework et, dans un autre terminal, tu lances l'app d'un client. Les deux
482
+ veulent 5151. Historiquement, la seconde mourait sur `EADDRINUSE`. En développement, ce n'est pas une
483
+ panne, c'est une nuisance.
484
+
485
+ ### La règle
486
+
487
+ `resolvePortPolicy()` (`portBinder.ts:74`) tranche selon l'environnement, et la valeur explicite gagne
488
+ toujours :
489
+
490
+ | Environnement | Défaut | Pourquoi |
491
+ | ------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
492
+ | `development` | `auto` | Un port pris est une gêne. L'app démarre sur le port suivant, et **le dit**. |
493
+ | `production` | `strict` | Le port est un **contrat** (service k8s, ingress, sonde). Glisser en silence = pod « sain » injoignable. |
494
+ | `test` | `strict` | Un port occupé signale un serveur resté debout ; le banc doit s'arrêter, pas taper sur le serveur du voisin. |
495
+
496
+ ### Le comportement observable
497
+
498
+ ```text
499
+ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
500
+ Figer le port : servers.http.port ; échouer au lieu de glisser :
501
+ servers.portPolicy = "strict".
502
+ ```
503
+
504
+ Le décalage est **toujours annoncé** (`server-http.ts:130`) : jamais de dégradation silencieuse. Trois
505
+ détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
506
+
507
+ 1. **On retente au `listen()`, jamais après une sonde.** Demander « le port est-il libre ? » puis
508
+ binder est une course : entre la réponse et le bind, un autre processus peut prendre le port. Le
509
+ `listen()` est atomique — on retente donc sur l'échec réel (`bindWithFallback()`,
510
+ `portBinder.ts:142`).
511
+ 2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
512
+ ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
513
+ sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:104`, réservation `portBinder.ts:111`).
514
+ 3. **Le gestionnaire d'erreur durable est posé APRÈS le bind.** Attaché avant, il verrait passer les
515
+ `EADDRINUSE` de repli et terminerait le kernel en croyant à une panne
516
+ (`ServerHttp.attachErrorHandler()`, `server-http.ts:186`).
517
+
518
+ Quand le bind échoue pour de bon — `strict`, ou tous les replis épuisés, ou une erreur qui n'est pas un
519
+ conflit de port — c'est **fatal** : message explicite puis terminaison du processus
520
+ (`ServerHttp.reportBindError()`, `server-http.ts:199`). Un serveur qui n'écoute pas ne doit jamais
521
+ laisser un processus se croire démarré.
522
+
523
+ ### Le corollaire : les ports effectifs sont publiés
524
+
525
+ Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
526
+ convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
527
+ que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1096`) écrit donc la
528
+ topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
529
+ environnements** : une application qui déclare son port via `PORT` (PaaS) sort aussi de la convention,
530
+ même en `strict`. L'écriture est au mieux-effort — une image en lecture seule ne fait jamais tomber un
531
+ serveur qui, lui, écoute très bien.
532
+
533
+ ## 🔐 Certificats TLS
534
+
535
+ ### La doctrine
536
+
537
+ **Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
538
+ n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
539
+ ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
540
+ (`Certificate.resolveStrategy()`, `certificates.ts:409`).
541
+
542
+ ### Les quatre stratégies
543
+
544
+ Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:448`), résolues par
545
+ `Certificate.resolveStrategy()` (`certificates.ts:370`).
546
+
547
+ | Stratégie | Quand l'utiliser | Ce qui se passe |
548
+ | --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
549
+ | `auto` (défaut) | On ne veut pas décider | `key`+`cert` fournis → `explicit` ; sinon mkcert en dev ; sinon auto-signé. |
550
+ | `explicit` | **Production** | Charge `key`/`cert`/`ca` depuis la config. Erreur au boot si absents. |
551
+ | `mkcert` | Développement avec HTTPS sans avertissement | CA locale de confiance → HMR cross-origin et WSS sans erreur navigateur. |
552
+ | `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé node-forge, non trusté. |
553
+
554
+ ```typescript
555
+ // nodefony.config.ts — production : certificat fourni, jamais généré
556
+ export default defineConfig(() => ({
557
+ modules: [
558
+ use("@nodefony/http", {
559
+ certificates: {
560
+ strategy: "explicit",
561
+ key: "/etc/tls/privkey.pem",
562
+ cert: "/etc/tls/fullchain.pem",
563
+ ca: "/etc/tls/chain.pem",
564
+ },
565
+ https: { rejectUnauthorized: true },
566
+ }),
567
+ "@nodefony/framework",
568
+ ],
569
+ }));
570
+ ```
571
+
572
+ > [!TIP]
573
+ > Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
574
+ > et au WSS) : `brew install mkcert nss && mkcert -install`. Nodefony le détecte tout seul, sinon il
575
+ > l'annonce et retombe sur l'auto-signé (`certificates.ts:392`).
576
+
577
+ ### Ce que le chemin `explicit` évite
578
+
579
+ `node-forge` est une grosse dépendance. Elle est chargée **paresseusement**, uniquement sur le chemin
580
+ de génération (`Certificate.loadForge()`, `certificates.ts:218`) : en production avec un certificat
581
+ fourni, elle n'entre jamais dans le processus (`certificates.ts:325`).
582
+
583
+ ### Conformité de l'auto-signé
584
+
585
+ Un certificat de développement bâclé fait perdre des heures (« not yet valid », « common name
586
+ invalid »). Celui de Nodefony respecte les règles qui comptent :
587
+
588
+ | Exigence | Norme | Mise en œuvre |
589
+ | --------------------------------------- | -------------------- | --------------------------------------------------------- |
590
+ | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `openssl.hash` par défaut `sha256` (`config.ts:379`) |
591
+ | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:255`) |
592
+ | Le SAN fait foi, pas le CN | RFC 6125 | SAN dérivé du kernel si non fourni (`config.ts:415`) |
593
+ | `notBefore` reculé (décalage d'horloge) | pratique | `openssl.backdateMinutes`, défaut 5 (`config.ts:394`) |
594
+ | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:472`) |
595
+
596
+ ### Régénération automatique
597
+
598
+ Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
599
+ (`certificates.ts:533`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
600
+ plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
601
+ contrôle laisserait un certificat obsolète en place indéfiniment.
602
+
603
+ ## 🛡️ Derrière un reverse-proxy
604
+
605
+ Dès qu'un proxy est devant l'application, trois questions se posent — et trois réglages y répondent.
606
+
607
+ ### « Quelle est la vraie IP du client ? » → `trustProxy`
608
+
609
+ Sans barrière, n'importe quel client peut envoyer `X-Forwarded-For: 1.2.3.4` et usurper son IP :
610
+ contournement de rate-limit, journaux d'audit falsifiés. Le défaut est donc **`false` — ces en-têtes
611
+ sont ignorés** (`config.ts:959`), et l'IP retenue est celle de la socket réelle, non falsifiable.
612
+
613
+ | Valeur | Sens |
614
+ | -------------------------------------------- | ------------------------------------------------------------------ |
615
+ | `false` (défaut) | Ne jamais faire confiance aux `X-Forwarded-*`. |
616
+ | `true` | Confiance totale — **uniquement** si le proxy est l'unique entrée. |
617
+ | IP, CIDR, liste | Confiance conditionnée à l'adresse de la socket. |
618
+ | `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
619
+
620
+ La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
621
+ `http-kernel.ts:538`, via `buildTrustProxy()`, `trustProxy.ts:100`) : aucune structure allouée par
622
+ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
623
+ (`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
624
+
625
+ ### « Ce `Host` est-il le mien ? » → `trustedHosts`
626
+
627
+ Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
628
+ kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
629
+ `http-kernel.ts:936`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
630
+ d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
631
+ filtre déjà le `Host` (`config.ts:968`).
632
+
633
+ ### « Cette page a-t-elle le droit d'ouvrir un WebSocket ? » → `allowedOrigins`
634
+
635
+ Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
636
+ WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
637
+ par défaut que l'`Origin` du handshake corresponde au `Host` servi
638
+ (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:599`), avec tolérance loopback en développement
639
+ et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
640
+ Violation)**, jamais par un code HTTP.
641
+
642
+ > [!NOTE]
643
+ > Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
644
+ > (`http-kernel.ts:551`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
645
+ > il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
646
+
647
+ ## Probes de santé — `/livez` et `/readyz`
648
+
649
+ Deux questions différentes, deux réponses différentes — les confondre casse les déploiements.
650
+
651
+ | Probe | Question | Réponse |
652
+ | --------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
653
+ | `/livez` | Le processus est-il vivant ? | `200` tant qu'il sert, **y compris pendant le drain**. |
654
+ | `/readyz` | Peut-il recevoir du trafic ? | `200` si le boot est complet, que l'arrêt n'a pas commencé **et** qu'aucun composant ne retient la mise en service ; `503` sinon. |
655
+
656
+ Le détail qui compte : `livez` reste à `200` pendant l'arrêt gracieux. Répondre `503` là ferait
657
+ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément ce qu'on essaie de
658
+ protéger.
659
+
660
+ Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
661
+ (`http-kernel.ts:944`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
662
+ réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:473`). Et surtout : **avant le
663
+ rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
664
+
665
+ | Option | Type | Défaut | Effet |
666
+ | --------------- | ------ | --------- | ---------------------------------------------------------------------- |
667
+ | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:919`). |
668
+ | `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
669
+ | `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
670
+ | `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
671
+
672
+ ### Retenir la mise en service — `kernel.setReadiness()`
673
+
674
+ Un pod peut être parfaitement démarré et incapable de servir : son schéma de base est en retard,
675
+ un cache n'est pas chaud, un service dont il dépend n'a pas encore répondu. Tout composant peut
676
+ alors **retenir** `/readyz`, et l'orchestrateur en tire la conséquence : il n'envoie pas de trafic,
677
+ l'ancien exemplaire continue de servir, le déploiement s'arrête proprement.
678
+
679
+ ```ts
680
+ // Dans un service, au boot puis à chaque re-vérification.
681
+ kernel.setReadiness("schema", false, "3 migrations en attente"); // → /readyz 503
682
+ // …le travail de migration s'applique (depuis l'extérieur, par un Job) :
683
+ kernel.setReadiness("schema", true); // → /readyz 200, sans redéploiement
684
+ kernel.clearReadiness("schema"); // le composant s'arrête : sa voix ne compte plus
685
+ ```
686
+
687
+ Trois règles à connaître :
688
+
689
+ - **Le verdict est déjà calculé.** `setReadiness` prend un booléen, jamais une fonction : la sonde
690
+ ne déclenche aucune vérification. Une sonde qui interroge une base tombe avec elle — celle-ci
691
+ lit un entier, et répond même quand la base est injoignable.
692
+ - **Un seul « pas prêt » suffit à retenir** ; il faut que tous soient prêts pour libérer. Un même
693
+ nom réenregistré ne compte qu'une voix.
694
+ - **`livez` reste à `200`.** Un schéma en retard est un état EXTERNE : redémarrer le processus ne
695
+ le répare pas. C'est aussi pourquoi le pod redevient disponible **tout seul** dès que la cause
696
+ est levée.
697
+
698
+ Ce que voit l'exploitant : le journal du pod NOMME ce qui retient (`CRITIC` à chaque bascule), le
699
+ plan d'administration publie `ready` et `readinessBlocked` (`GET /nodefony/kernel/api/livez` ; les
700
+ noms sont réservés à un appelant authentifié), et `nodefony status` affiche une ligne
701
+ `disponibilité`. Le corps de `/readyz`, lui, reste une constante — c'est le prix à ne pas payer sur
702
+ un chemin sondé toutes les 2 à 10 secondes.
703
+
704
+ Le match est **strict** sur l'URL brute : `/livez?x=1` repart dans le pipeline normal. Les deux
705
+ serveurs (HTTP et HTTPS) servent ces chemins — un kubelet configuré en `scheme: HTTPS` fonctionne.
706
+
707
+ ## Arrêt gracieux — la séquence du SIGTERM
708
+
709
+ Un arrêt brutal coupe les requêtes en vol : à chaque mise à jour progressive, des utilisateurs voient
710
+ une erreur. L'arrêt de Nodefony est une **séquence ordonnée**, et l'ordre est le cœur du sujet.
711
+
712
+ ```mermaid
713
+ sequenceDiagram
714
+ participant O as Orchestrateur
715
+ participant K as Kernel
716
+ participant W as server-websocket
717
+ participant H as server-http / https
718
+ O->>K: SIGTERM
719
+ K->>K: readyz → 503 (+ shutdownDelay)
720
+ Note over K: le load balancer retire le pod
721
+ K->>W: onTerminate (prepend)
722
+ W->>W: close 1001 « Going Away » à chaque client
723
+ Note over W: fenêtre ~300 ms
724
+ K->>H: onTerminate (once)
725
+ H->>H: drain — in-flight terminées, sockets idle fermées
726
+ Note over H: destruction forcée après shutdownTimeout
727
+ K->>O: exit 0
728
+ ```
729
+
730
+ **Étape 1 — la disponibilité bascule en premier.** L'écouteur est posé à `onPostReady` pour être
731
+ inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:515`). `readyz` répond `503`, le
732
+ load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
733
+ propager.
734
+
735
+ **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:117`)
736
+ envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:134`).
737
+ Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
738
+ indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
739
+
740
+ **Étape 3 — les requêtes HTTP se terminent.** Le drain est délégué à `http-terminator`
741
+ (`createDrainTerminator()`, `serverShutdown.ts:25`) : en-tête `connection: close` injecté sur les
742
+ réponses en cours, sockets inactives fermées, destruction forcée au-delà de `shutdownTimeout` (défaut
743
+ `5000` ms, `serverShutdown.ts:38`).
744
+
745
+ > [!IMPORTANT]
746
+ > L'ordre WS-avant-HTTP n'est pas cosmétique. Les serveurs WebSocket s'inscrivent en
747
+ > `prependOnceListener` (`server-websocket.ts:91`), les serveurs HTTP en `once`
748
+ > (`server-http.ts:151`). Inversé, le terminator détruirait les sockets déjà upgradées **sans** frame
749
+ > Close : retour du 1006 pour tous les clients temps réel.
750
+
751
+ Deux garde-fous encadrent la séquence : `shutdownTimeout` **par serveur** (le drain nominal) et
752
+ `shutdownDeadline` **global** au kernel (défaut 15 s, `src/nodefony/src/config/defaults.ts:46`), filet
753
+ anti-écouteur bloqué. Les deux doivent rester sous la période de grâce de l'orchestrateur — 30 s en
754
+ Kubernetes, 10 s avec Docker.
755
+
756
+ ## Résilience — ce qui ne doit jamais tuer le processus
757
+
758
+ Un serveur runtime se juge à ce qu'il fait des cas anormaux. Quatre défenses vivent au niveau du
759
+ transport.
760
+
761
+ ### En-têtes malformés — le piège `clientError` de Node
762
+
763
+ Dès qu'un écouteur `clientError` est attaché, **Node cesse de fermer la socket automatiquement**. Un
764
+ écouteur naïf (« je journalise et je passe ») transforme donc une requête malformée en **fuite de
765
+ socket et de descripteur de fichier** — c'est-à-dire un vecteur de déni de service. Nodefony répond et
766
+ ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les en-têtes débordent
767
+ (RFC 6585 §5), `400` sinon (`clientError.ts:25`), et rien du tout si la socket est déjà morte.
768
+
769
+ ### Connexions WebSocket zombies
770
+
771
+ Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
772
+ descripteur retenus pour personne. Le battement de cœur les réclame
773
+ (`startHeartbeat()`, `wsHeartbeat.ts:69`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
774
+ les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts:98`).
775
+
776
+ L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
777
+ serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
778
+ (`trackPong()`, `wsHeartbeat.ts:41`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
779
+ horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
780
+ configuration pathologique (`tick`, `wsHeartbeat.ts:81`) ; et un timer `unref` qui ne retient jamais le
781
+ processus à l'arrêt.
782
+
783
+ ### Floods de connexions
784
+
785
+ L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
786
+ IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
787
+ (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1497`). Le `101` étant déjà émis par `ws`, un `429`
788
+ est impossible → la connexion est fermée en **1013 « Try Again Later »**
789
+ (`rateLimiter`, `http-kernel.ts:287`), sans
790
+ journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
791
+
792
+ Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
793
+ `wsMaxConnectionsPerIp` (`config.ts:1046`). En cloud-native, laisser `null` et déléguer à l'edge —
794
+ nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
795
+ descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
796
+
797
+ ### Bornes de payload
798
+
799
+ `maxPayload` (1 MiB par défaut) fait fermer en **1009 « Message Too Big »** ; `maxBackpressure`
800
+ (4 MiB) protège contre le client **lent à recevoir**, dont le tampon d'envoi gonflerait jusqu'à
801
+ l'OOM — un seul lent peut plomber une diffusion générale. La politique par défaut, `drop`, saute la
802
+ frame et garde le client connecté.
803
+
804
+ ## ⚡ Performance & mémoire
805
+
806
+ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié par le nombre de requêtes
807
+ par seconde. Les choix visibles dans le code :
808
+
809
+ - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
810
+ socket (`wsHeartbeat.ts:69`).
811
+ - **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
812
+ `trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:937`, `http-kernel.ts:489`).
813
+ - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
814
+ DI et l'ALS : un flood coûte une recherche dans une table de hachage.
815
+ - **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
816
+ (`http-kernel.ts:383`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
817
+ - **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
818
+ route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
819
+ production mono-processus).
820
+ - **`node-forge` jamais chargé en production** avec un certificat fourni (`certificates.ts:218`).
821
+
822
+ Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
823
+ boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
824
+ plus de 750 connexions simultanées et 33 000 à 38 000 messages/s soutenus. Rejouer ces mesures : skill
825
+ `nodefony-load-test`. Gate mémoire avant tout commit touchant le pipeline : `npm run test:memory`
826
+ (skill `nodefony-check-memory-health`).
827
+
828
+ **Mise à l'échelle.** Un processus = un pod. Le passage à l'échelle est horizontal, confié à
829
+ l'orchestrateur ; sur une seule machine, `nodefony cluster -w N` fork des workers. Attention à la
830
+ conséquence : `broadcast()` ne touche que les clients du **même** worker — un fan-out inter-processus
831
+ demande le backplane realtime.
832
+
833
+ ## 📜 Normes appliquées
834
+
835
+ | Domaine | Norme | Ancrage |
836
+ | ------------------------------------- | ------------------ | -------------------------------------------------------------------------- |
837
+ | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:819`) |
838
+ | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:174`) |
839
+ | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:334`) |
840
+ | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:25`) |
841
+ | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:490`) |
842
+ | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:134`) |
843
+ | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:516`) |
844
+ | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:596`) |
845
+ | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:540`) |
846
+ | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:599`) |
847
+ | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
848
+ | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:255`) |
849
+ | Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:415`) |
850
+
851
+ ## ⚠️ Pièges (symptôme → cause → correction)
852
+
853
+ | Symptôme | Cause | Correction |
854
+ | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
855
+ | L'app écoute sur 5153 au lieu de 5151 | `portPolicy: "auto"` (défaut dev) : le port était pris — c'est **annoncé** | Libérer le port, ou `servers.portPolicy: "strict"` pour échouer franchement |
856
+ | En production, le pod est « sain » mais injoignable | Un port glissé en silence | Rien à faire : `strict` est le défaut hors dev — vérifier qu'on ne l'a pas forcé |
857
+ | `nodefony status` ne voit pas le serveur | Ports sondés par convention alors qu'ils ont glissé | Déjà géré : les ports effectifs sont publiés (`http-kernel.ts:215`) |
858
+ | Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
859
+ | Plus de WebSocket après avoir mis `servers.https: false` | Le WSS est adossé au serveur HTTPS et tombe avec lui | Attendu — utiliser `ws://` sur le port clair, ou réactiver HTTPS |
860
+ | Le client WebSocket voit `1006` à chaque redéploiement | Frame Close jamais reçue (socket coupée avant) | Déjà géré : close `1001` avant le drain (`server-websocket.ts:134`) |
861
+ | Requêtes coupées à chaque mise à jour progressive | Destruction des connexions au lieu d'un drain | Déjà géré (`createDrainTerminator()`) — vérifier `shutdownTimeout` < grâce k8s |
862
+ | Le pod redémarre en boucle pendant l'arrêt | `livez` répondrait `503` pendant le drain | Déjà géré : `livez` reste `200`, seul `readyz` bascule |
863
+ | Cascade de redémarrages sous charge | Sonde de santé soumise au rate-limit | Déjà géré : les probes court-circuitent avant le rate-limit (`http-kernel.ts:848`) |
864
+ | `curl --http2` renvoie du HTTP/1.1 | `servers.https.protocol: "1.1"`, ou client sans ALPN | Passer `protocol: "2.0"` (défaut) et vérifier le client |
865
+ | Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
866
+ | Le certificat n'est pas régénéré après un changement de domaine | On croit qu'un fichier présent suffit | Déjà géré : le SAN est vérifié (`certificates.ts:546`) |
867
+ | `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux |
868
+ | Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
869
+ | Handshake WebSocket refusé en `1008` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
870
+ | Fermetures WebSocket en `1013` inexpliquées | Rate-limit d'IP, ou plafond de connexions concurrentes | Vérifier `rateLimit` et `wsMaxConnectionsPerIp` |
871
+
872
+ ## 📡 Observabilité — Studio et CLI
873
+
874
+ **Studio.** L'onglet **Configuration** rend la config du module en réglages documentés (type, défaut,
875
+ état, valeur effective) à partir du JSON Schema dérivé du schéma Zod
876
+ (`src/packages/@nodefony/http/index.ts:79`). Le profiler expose les phases par requête, y compris
877
+ l'origine du transport.
878
+
879
+ **CLI.** Trois commandes touchent directement aux serveurs :
880
+
881
+ | Commande | Rôle |
882
+ | ---------------------------------------------------- | ------------------------------------------------------------------------ |
883
+ | `nodefony http:network [interface] [--json]` | Interfaces réseau vues par le kernel (`networkCommand.ts:18`). |
884
+ | `nodefony http:certificates [--force] [--json]` | (Re)génère et décrit le certificat de dev (`certificatesCommand.ts:24`). |
885
+ | `nodefony proxy:generate <nginx\|haproxy> [-o file]` | Dérive une configuration reverse-proxy de l'introspection réelle. |
886
+
887
+ `proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
888
+ confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
889
+ résumé de certificat vient de `Certificate.describe()` (`certificates.ts:803`), source unique partagée
890
+ par la commande, le boot et un futur écran d'administration.
891
+
892
+ **Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
893
+ fonctionnent donc même après un décalage de port.
894
+
895
+ ## 🧪 Tests & couverture
896
+
897
+ Les chiffres exacts vivent dans la carte de tests de cette page (régénérée depuis vitest, jamais figés
898
+ dans le Markdown).
899
+
900
+ | Type | Où |
901
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
902
+ | Unitaires | `unit/certificates.test.ts` (stratégies, conformité), `unit/trustProxy.test.ts` (CIDR, préréglages, `BlockList`), `unit/generateProxyConfig.test.ts` |
903
+ | Unitaires — ports | `unit/portBinder.test.ts` — repli sur de **vraies** sockets (un `listen` simulé ne prouverait rien du noyau) |
904
+ | Intégration HTTP | `http/http.test.ts`, `http/http1.test.ts`, `http/httpKernel.test.ts` (pipeline, en-têtes, `X-Request-Id`) |
905
+ | Intégration TLS | `http/https.test.ts` — handshake, version TLS, chiffrement, SAN `localhost`, HSTS |
906
+ | Intégration santé | `http/health.test.ts` — `/livez` et `/readyz` sur HTTP **et** HTTPS, absence de `Set-Cookie` |
907
+ | Intégration Host | `http/host-misdirected.test.ts` — barrière `trustedHosts` |
908
+ | **Résilience** | `http/resilience.test.ts` — déconnexions abruptes, corps surdimensionnés, en-têtes malformés (`clientError`), rafales, aborts en pleine réponse HTTP/2 |
909
+ | Timeouts / aborts | `http/timeout-abort.test.ts`, `http/abort-cleanup.test.ts`, `http/client-abort-499.test.ts` |
910
+ | WebSocket | `websockets/websocket.test.ts`, `websocket-protocol.test.ts`, `websocket-limits.test.ts`, `websocket-binary-broadcast.test.ts` |
911
+ | **Charge / mémoire** | `tests/load/ws-connections-load.test.ts` (plafond de sockets), `ws-messages-load.test.ts` (débit + diffusion), `http/memory.test.ts` (gate mémoire) |
912
+ | **E2E d'arrêt** | `nodefony-load-test` → `run.sh graceful` — `readyz` à 503, requête en vol servie, WS fermé en 1001, port libéré |
913
+ | **E2E de bornes** | `run.sh ws-handshake-rl` (rate-limit du handshake), `run.sh ws-conn-cap` (plafond de connexions par IP) |
914
+
915
+ Ce qui **manque** aujourd'hui : aucun test d'intégration ne couvre le décalage de port de bout en bout
916
+ (le repli est prouvé unitairement sur de vraies sockets, pas via un boot complet), et il n'existe pas de
917
+ banc dédié au keep-alive WebSocket (la détection de zombie est exercée indirectement par les tests de
918
+ charge).
919
+
920
+ Suites : `npm test` (unitaires), `npm run test:integration` (serveur requis), `npm run test:load` et
921
+ `npm run test:memory` (serveur lancé avec `--expose-gc`). Couverture : `npm run coverage` dans
922
+ `@nodefony/http` — le pourcentage vit dans le rapport vitest, jamais figé ici. Skills associés :
923
+ `nodefony-load-test`, `nodefony-check-memory-health`, `nodefony-security-review`.
924
+
925
+ ## 🔗 Pour aller plus loin
926
+
927
+ - ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
928
+ - 🧭 **Pages sœurs** : [Sessions](session.md)
929
+ - Le pipeline qui reçoit ce que les serveurs transmettent → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
930
+ - Déployer en conteneur (probes, arrêt gracieux, TLS à l'ingress) → [docker-cloud-native](../../../../../docs/guides/docker-cloud-native.md)
931
+ - Configuration d'application (`defineConfig`, `use`, env) → [configuration](../../../../../docs/guides/configuration.md)
932
+ - Zones, authentification et autorisation par-dessus le transport → [Firewall](../../security/docs/firewall.md)
933
+ - En-têtes de sécurité applicatifs (CSP, Referrer-Policy…) → [En-têtes](../../security/docs/headers.md)
934
+ - WebSocket applicatif : canaux, diffusion, backplane → [@nodefony/realtime](../../realtime/docs/index.md)
935
+ - Vue d'ensemble de l'architecture → [vue-ensemble](../../../../../docs/architecture/vue-ensemble.md)