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

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 (138) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +13 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +3 -3
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  12. package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
  13. package/dist/nodefony/service/audit-logger.js +23 -30
  14. package/dist/nodefony/service/certificates.js +97 -13
  15. package/dist/nodefony/service/error-renderer.js +29 -12
  16. package/dist/nodefony/service/http-kernel.js +198 -50
  17. package/dist/nodefony/service/pretty-request-logger.js +15 -13
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +3 -3
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +3 -3
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  29. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +78 -43
  36. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  37. package/dist/nodefony/src/context/forwarded.js +1 -2
  38. package/dist/nodefony/src/context/http/HttpContext.js +28 -6
  39. package/dist/nodefony/src/context/http/Request.js +33 -29
  40. package/dist/nodefony/src/context/http/Response.js +23 -6
  41. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +41 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/requestTrace.js +104 -0
  49. package/dist/nodefony/src/context/responseEnded.js +1 -2
  50. package/dist/nodefony/src/context/timingMode.js +21 -0
  51. package/dist/nodefony/src/context/transport.js +45 -0
  52. package/dist/nodefony/src/context/trustProxy.js +1 -2
  53. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  54. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  55. package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
  56. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  57. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  58. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  59. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  60. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  61. package/dist/nodefony/src/cookies/cookie.js +3 -7
  62. package/dist/nodefony/src/errors/httpError.js +1 -2
  63. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  64. package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
  65. package/dist/nodefony/src/profiler/Profiler.js +104 -32
  66. package/dist/nodefony/src/profiler/aggregate.js +442 -0
  67. package/dist/nodefony/src/proxy/forward.js +29 -5
  68. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  69. package/dist/nodefony/src/proxy/rules.js +12 -6
  70. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  71. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  72. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  73. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  74. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  75. package/dist/nodefony/src/servers/portBinder.js +41 -10
  76. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  77. package/dist/nodefony/src/session/session.js +2 -3
  78. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  79. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  80. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  81. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  82. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  83. package/dist/types/index.d.ts +9 -3
  84. package/dist/types/nodefony/config/config.d.ts +4 -0
  85. package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
  86. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  87. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  88. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  89. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  91. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
  92. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  93. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  94. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  95. package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
  96. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  97. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  98. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  99. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  100. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  101. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  102. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  103. package/dist/types/nodefony/src/context/Context.d.ts +38 -5
  104. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
  105. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  106. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  107. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  108. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  109. package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
  110. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  111. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  112. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  113. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
  118. package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
  119. package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
  120. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  121. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  122. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  123. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  124. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  125. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  126. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  127. package/docs/cookies.md +85 -78
  128. package/docs/diagnostics-channel.md +303 -0
  129. package/docs/index.md +6 -1
  130. package/docs/observabilite.md +159 -81
  131. package/docs/rate-limit.md +83 -83
  132. package/docs/reverse-proxy.md +72 -70
  133. package/docs/servers.md +338 -224
  134. package/docs/session.md +179 -179
  135. package/docs/sse.md +24 -20
  136. package/docs/upload.md +120 -117
  137. package/package.json +8 -9
  138. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/servers.md CHANGED
@@ -51,7 +51,7 @@ flowchart TD
51
51
  INIT -->|"servers.https ≠ false"| SS["server-https<br/>node:https ou node:http2 — 5152"]
52
52
  SH --> WS["server-websocket<br/>ws:// sur 5151"]
53
53
  SS --> WSS["server-websocket-secure<br/>wss:// sur 5152"]
54
- SH --> HK["HttpKernel<br/>onHttpRequest / onWebsocketRequest"]
54
+ SH --> HK["HttpKernel<br/>onHttpRequest / onWebsocketUpgrade"]
55
55
  SS --> HK
56
56
  WS --> HK
57
57
  WSS --> HK
@@ -61,13 +61,13 @@ flowchart TD
61
61
  Trois idées à retenir, et tout le reste en découle :
62
62
 
63
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
64
+ (`src/packages/@nodefony/http/index.ts#nodefony`) et instanciés par le conteneur, comme n'importe quel
65
65
  service — donc introspectables, testables, remplaçables.
66
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
67
+ (`Websocket.createServer()`, `server-websocket.ts#Websocket.createServer`) ; `server-websocket-secure` reçoit le
68
68
  `https.Server`. Couper HTTPS coupe donc le WSS, sans autre réglage.
69
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.
70
+ sessions, ni le firewall : ils branchent `onHttpRequest` / `onWebsocketUpgrade` et se taisent.
71
71
 
72
72
  ## 📖 Lexique
73
73
 
@@ -122,18 +122,18 @@ les probes.
122
122
 
123
123
  **Un port TLS qui parle deux protocoles.** Quand `servers.https.protocol` vaut `"2.0"` (le défaut),
124
124
  Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttps.createServerH2()`,
125
- `server-https.ts:187`) : les clients modernes négocient `h2` par ALPN, les autres restent en HTTP/1.1
125
+ `server-https.ts#ServerHttps.createServerH2`) : les clients modernes négocient `h2` par ALPN, les autres restent en HTTP/1.1
126
126
  **sur le même port**. Le protocole effectif est relu socket par socket pour taguer le contexte
127
- (`server-https.ts:222`).
127
+ (`server-https.ts#ServerHttps.createServerH2`).
128
128
 
129
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:1964`) —, hérite de la même
130
+ (`server-websocket.ts#Websocket.createServer`), passe par le **même** rate-limit d'IP que les requêtes HTTP — un upgrade
131
+ _est_ une requête HTTP (`HttpKernel.onWebsocketUpgrade()`, `http-kernel.ts#HttpKernel.onWebsocketUpgrade`) —, hérite de la même
132
132
  session et du même firewall, et se ferme avec le même soin qu'une réponse HTTP.
133
133
 
134
134
  > [!NOTE]
135
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:1216`). Elle ne fait rien aujourd'hui.
136
+ > marquée `reserved` (`config.ts#httpConfigSchema`). Elle ne fait rien aujourd'hui.
137
137
 
138
138
  ## 🚀 Démarrage rapide
139
139
 
@@ -164,45 +164,45 @@ export default defineConfig((ctx) => ({
164
164
 
165
165
  ### 2. Le réglage — le comportement de chaque serveur
166
166
 
167
- Le réglage appartient au **module**, colocalisé dans le manifeste via `use()`. Toutes les clés
167
+ Le réglage appartient au **module** : il vit dans `nodefony/config/http.ts`, que le manifeste passe
168
+ à `use("@nodefony/http", …)`. Toutes les clés
168
169
  ci-dessous sont facultatives : ce sont les défauts du schéma Zod, écrits ici pour les montrer.
169
170
 
170
171
  ```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
- }));
172
+ // nodefony/config/http.ts — réglage transport, le fragment que
173
+ // `nodefony.config.ts` passe à `use("@nodefony/http", httpConfig())`
174
+ import type { IHttpConfigInput } from "@nodefony/http";
175
+
176
+ export const httpConfig = () =>
177
+ ({
178
+ // Ne pas exposer l'identité du serveur (anti-empreinte, recommandé en prod).
179
+ headerServer: null,
180
+ // Barrière Host : le domaine canonique est toujours accepté, + ceux-ci.
181
+ trustedHosts: ["app.example.com"],
182
+ // Un seul reverse-proxy en amont ? alors et alors seulement, lui faire confiance.
183
+ trustProxy: false,
184
+ http: {
185
+ requestTimeout: 30_000, // anti slow-loris (réception complète)
186
+ keepAliveTimeout: 5_000, // réutilisation de la socket TCP
187
+ shutdownTimeout: 5_000, // drain avant destruction forcée
188
+ },
189
+ http2: {
190
+ maxConcurrentStreams: 100, // défense CVE-2023-44487 (Rapid Reset)
191
+ maxSessionMemory: 10, // Mo par session h2
192
+ },
193
+ websocket: {
194
+ maxPayload: 1024 * 1024, // 1 MiB → au-delà, close 1009
195
+ keepaliveInterval: 20_000, // ping toutes les 20 s
196
+ keepaliveGracePeriod: 10_000, // pong attendu sous 10 s, sinon zombie
197
+ },
198
+ // Probes cloud-native — actives par défaut.
199
+ health: {
200
+ enabled: true,
201
+ livenessPath: "/livez",
202
+ readinessPath: "/readyz",
203
+ },
204
+ certificates: { strategy: "auto" }, // mkcert en dev, sinon auto-signé
205
+ }) satisfies IHttpConfigInput;
206
206
  ```
207
207
 
208
208
  ### 3. Le contrôleur qui répond sur les deux transports
@@ -225,8 +225,9 @@ class PingController extends Controller {
225
225
  @Get("/")
226
226
  async ping() {
227
227
  return this.renderJson({
228
- scheme: this.context?.scheme, // "http" | "https"
229
- type: this.context?.type, // "http" | "http2" | "websocket"
228
+ type: this.context?.type, // "http" | "websocket" — la famille
229
+ scheme: this.context?.scheme, // "http" | "https" — vu par le client
230
+ alpnProtocol: this.context?.alpnProtocol, // "http/1.1" | "h2" | "h2c"
230
231
  });
231
232
  }
232
233
 
@@ -249,7 +250,7 @@ export default PingController;
249
250
 
250
251
  ### 4. Ce qu'on observe au boot
251
252
 
252
- Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1355`). En développement, le
253
+ Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts#Kernel.onReady`). En développement, le
253
254
  bilan de démarrage (`renderStartupHuman`, `startupScreen.ts`) dit d'abord **où aller**,
254
255
  puis **ce qui est à regarder**, puis les adresses d'écoute :
255
256
 
@@ -266,14 +267,14 @@ puis **ce qui est à regarder**, puis les adresses d'écoute :
266
267
  ```
267
268
 
268
269
  L'adresse à **ouvrir** n'est pas l'adresse de **liaison** : un serveur lié à `127.0.0.1` ou à
269
- `0.0.0.0` s'ouvre sur `localhost` (`openableHost`, `bootReport.ts:216`) — les passkeys refusent
270
+ `0.0.0.0` s'ouvre sur `localhost` (`openableHost`, `bootReport.ts#openableHost`) — les passkeys refusent
270
271
  une IP comme domaine, et un cookie posé sous un nom ne part pas sous l'autre. Hors d'un
271
272
  terminal, le même bilan sort ligne à ligne (`open.app: https://localhost:5152/`,
272
273
  `listen.https: 127.0.0.1:5152`) ; pour une machine, `nodefony status --json` le rend en entier.
273
274
 
274
275
  Hors écran animé (production, CI, `--debug`, sortie non-terminal), les bannières par serveur
275
- sortent aussi (`ServerHttp.showBanner()`, `server-http.ts:239`, appelées par le kernel —
276
- `Kernel.ts:1403`) :
276
+ sortent aussi (`ServerHttp.showBanner()`, `server-http.ts#ServerHttp.showBanner`, appelées par le kernel —
277
+ `Kernel.ts#Kernel.onReady`) :
277
278
 
278
279
  ```text
279
280
  Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
@@ -284,15 +285,15 @@ Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
284
285
  ```bash
285
286
  # HTTP/1.1 en clair
286
287
  curl -s http://127.0.0.1:5151/ping
287
- # {"scheme":"http","type":"http"}
288
+ # {"type":"http","scheme":"http","alpnProtocol":"http/1.1"}
288
289
 
289
290
  # HTTP/2 sur le port TLS (-k : le certificat de dev n'est pas dans le trust store)
290
291
  curl -sk --http2 https://127.0.0.1:5152/ping
291
- # {"scheme":"https","type":"http2"}
292
+ # {"type":"http","scheme":"https","alpnProtocol":"h2"}
292
293
 
293
294
  # Le même port TLS sert encore HTTP/1.1 (allowHTTP1) — repli négocié par ALPN
294
295
  curl -sk --http1.1 https://127.0.0.1:5152/ping
295
- # {"scheme":"https","type":"https"}
296
+ # {"type":"http","scheme":"https","alpnProtocol":"http/1.1"}
296
297
 
297
298
  # Probes cloud-native, servies par les DEUX serveurs
298
299
  curl -si http://127.0.0.1:5151/livez | head -1 # HTTP/1.1 200 OK
@@ -305,26 +306,36 @@ C'est le déploiement **nominal** en Kubernetes : l'ingress porte le certificat,
305
306
  `https: false` désactive HTTPS **et** le WSS qui en hérite, et supprime au passage toute génération de
306
307
  certificat au boot.
307
308
 
308
- ```typescript
309
+ ```typescript ignore
309
310
  // nodefony.config.ts — pod k8s derrière un ingress qui termine le TLS
311
+ import { defineConfig, use } from "nodefony";
312
+ import { httpConfig } from "./nodefony/config/http";
313
+
310
314
  export default defineConfig((ctx) => ({
311
315
  domain: ctx.isProd ? "0.0.0.0" : "127.0.0.1",
312
316
  servers: {
313
317
  https: false, // un seul port exposé : 5151
314
318
  },
315
- modules: [
316
- use("@nodefony/http", {
317
- // L'ingress est l'unique point d'entrée → ses X-Forwarded-* sont fiables.
318
- trustProxy: ctx.isProd ? "uniquelocal" : false,
319
- // L'ingress filtre déjà le Host ; sinon lister les vhosts servis.
320
- trustedHosts: ["app.example.com"],
321
- }),
322
- "@nodefony/framework",
323
- ],
319
+ modules: [use("@nodefony/http", httpConfig(ctx)), "@nodefony/framework"],
324
320
  }));
325
321
  ```
326
322
 
327
- ## 🔌 Les quatre serveurs (et le cinquième service)
323
+ ```typescript ignore
324
+ // nodefony/config/http.ts — le fragment que le manifeste ci-dessus appelle
325
+ import type { ConfigContext } from "nodefony";
326
+ import type { IHttpConfigInput } from "@nodefony/http";
327
+ import type { env } from "../../env";
328
+
329
+ export const httpConfig = (ctx: ConfigContext<typeof env>) =>
330
+ ({
331
+ // L'ingress est l'unique point d'entrée → ses X-Forwarded-* sont fiables.
332
+ trustProxy: ctx.isProd ? "uniquelocal" : false,
333
+ // L'ingress filtre déjà le Host ; sinon lister les vhosts servis.
334
+ trustedHosts: ["app.example.com"],
335
+ }) satisfies IHttpConfigInput;
336
+ ```
337
+
338
+ ## 🔌 Les serveurs — quatre par défaut, h2c en option
328
339
 
329
340
  Choisir en cinq secondes :
330
341
 
@@ -334,65 +345,142 @@ Choisir en cinq secondes :
334
345
  | `server-https` | `https://` — 5152 | `servers.https !== false` | TLS : HTTP/2 (défaut) ou HTTP/1.1. |
335
346
  | `server-websocket` | `ws://` — 5151 | `server-http` actif | WebSocket adossé au serveur HTTP. |
336
347
  | `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
348
+ | `server-h2c` | h2c — 5153 | `servers.h2c.enabled` | HTTP/2 en clair, pour un proxy inverse (option). |
337
349
  | `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
338
350
 
339
- L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1223`) : chaque serveur est
340
- consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:1223`).
341
- Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
351
+ L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts#HttpKernel.initServers`) : chaque serveur est
352
+ consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts#HttpKernel.initServers`).
353
+ Les serveurs WebSocket ne sont montés que si leur porteur l'a été. `server-h2c` n'a pas de WebSocket
354
+ adossé (voir sa section).
342
355
 
343
356
  ### `server-http` — HTTP/1.1 en clair
344
357
 
345
358
  Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.createServer()`
346
- (`server-http.ts:83`) crée le `http.Server`, applique les réglages de transport, branche
359
+ (`server-http.ts#ServerHttp.createServer`) crée le `http.Server`, applique les réglages de transport, branche
347
360
  `onHttpRequest`, puis écoute selon la politique de port.
348
361
 
349
362
  - **Actif** si `servers.http` n'est pas `false` — la décision est prise dès le constructeur
350
- (`server-http.ts:56`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
351
- `server-http.ts:74`).
363
+ (`server-http.ts#ServerHttp.constructor`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
364
+ `server-http.ts#ServerHttp.setPort`).
352
365
  - **Réglages appliqués** : `requestTimeout` (anti slow-loris), `maxHeadersCount`, `timeout` de socket
353
366
  (qui émet un événement `onTimeout`), `keepAliveTimeout`.
354
367
  - **Erreurs de protocole** : l'événement `clientError` est traité explicitement
355
- (`server-http.ts:186`) — voir la section Résilience, c'est un piège Node à part entière.
368
+ (`server-http.ts#ServerHttp.createServer`) — voir la section Résilience, c'est un piège Node à part entière.
356
369
 
357
370
  ### `server-https` — TLS, et HTTP/2 par défaut
358
371
 
359
- Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:100`) :
372
+ Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts#ServerHttps.protocol`) :
360
373
 
361
374
  - **`"2.0"` (défaut)** → `http2.createSecureServer` avec `allowHTTP1: true`
362
- (`ServerHttps.createServerH2()`, `server-https.ts:187`). Les bornes anti-DoS HTTP/2 ne sont posées
375
+ (`ServerHttps.createServerH2()`, `server-https.ts#ServerHttps.createServerH2`). Les bornes anti-DoS HTTP/2 ne sont posées
363
376
  **que si elles sont configurées**, pour ne pas écraser les défauts de Node
364
- (`maxSessionMemory`, `server-https.ts:215`).
377
+ (`maxSessionMemory`, `server-https.ts#ServerHttps.createServerH2`).
365
378
  Les erreurs de session et de flux sont journalisées sans tuer le serveur
366
- (`sessionError`, `server-https.ts:296`).
379
+ (`sessionError`, `server-https.ts#ServerHttps.listenWithPolicy`).
367
380
  - **`"1.1"`** → `https.createServer` classique.
368
381
 
369
382
  Le certificat vient du service `certificates`, lu au moment de la création via
370
- `serviceCerticats` (`server-https.ts:94`) — voir la section dédiée.
383
+ `serviceCerticats` (`server-https.ts#ServerHttps.createServer`) — voir la section dédiée.
371
384
 
372
385
  > [!IMPORTANT]
373
- > Sur la branche HTTP/2, le protocole **effectif** est relu par requête via l'ALPN de la socket TLS
374
- > (`server-https.ts:222`) : un client `h2` produit un contexte de type `http2`, un client HTTP/1.1 sur
375
- > le même port produit un contexte `https`. C'est ce qui rend le port TLS universel.
386
+ > Sur la branche HTTP/2, le protocole **effectif** se constate par requête (`transport.ts#alpnProtocolOf`) :
387
+ > un client `h2` produit un contexte `alpnProtocol: "h2"`, un client HTTP/1.1 sur le même port
388
+ > `alpnProtocol: "http/1.1"` — les deux avec `type: "http"` et `serverType: "https"`. C'est ce qui rend
389
+ > le port TLS universel.
390
+
391
+ ### `server-h2c` — HTTP/2 en clair, pour un proxy inverse
392
+
393
+ Un proxy inverse qui termine le TLS (Envoy et Istio, HAProxy, Traefik) peut parler HTTP/2 **en clair** à
394
+ ses amonts : c'est h2c, en connaissance préalable (RFC 9113 §3.3) — le client envoie directement la préface
395
+ HTTP/2, sans négociation. `ServerH2c.createServer()` (`server-h2c.ts#ServerH2c.createServer`) le sert sur un
396
+ port **dédié**, désactivé par défaut :
397
+
398
+ ```ts
399
+ // nodefony.config.ts
400
+ servers: { h2c: { enabled: true } }, // port 5153 par défaut — ou NF__APP__SERVERS__H2C__ENABLED=true
401
+ ```
402
+
403
+ Il emprunte le chemin HTTP/2 du port TLS — même pipeline, mêmes limites anti-DoS
404
+ (`http2Limits.ts#http2ServerLimits`), même arrêt par GOAWAY — sans TLS : le contexte porte
405
+ `alpnProtocol: "h2c"`, `serverType: "http"`, et son `scheme` vient du transport ou d'un proxy de confiance
406
+ (`trustProxy`), **jamais** du pseudo-en-tête `:scheme`, que le client écrit lui-même.
407
+
408
+ **Ce qu'il ne fait pas, délibérément :**
409
+
410
+ - **HTTP/1.1** : un client HTTP/1.1 échoue sur la préface et la session est fermée. Les sondes de santé et
411
+ les clients HTTP/1.1 restent sur le port HTTP — d'où l'avertissement au boot si `servers.http` vaut
412
+ `false` alors que h2c est actif.
413
+ - **WebSocket** : WebSocket sur HTTP/2 passe par le CONNECT étendu (RFC 8441), que ce serveur ne sert pas.
414
+ Le proxy route `Upgrade: websocket` vers le port HTTP.
415
+ - **La bascule `Upgrade: h2c`** depuis HTTP/1.1 : retirée par RFC 9113 §3.1 (contrebande h2c). Le port
416
+ HTTP la refuse — 404 sur un chemin sans route WebSocket, 400 sinon — et ne répond jamais `101`.
417
+ - **Partager un port** : `servers.h2c.port` égal à `servers.http.port` ou `servers.https.port` est refusé
418
+ au boot (`portBinder.ts#h2cPortConflict`).
419
+
420
+ #### Faut-il l'activer ? — h2c n'est pas « plus rapide » par principe
421
+
422
+ Le gain classique de HTTP/2 est le multiplexage : plusieurs requêtes sur une connexion, sans blocage en tête
423
+ de file. Il sert surtout un **navigateur**, limité à quelques connexions sur un réseau lent. Entre un proxy et
424
+ un pod, sur un réseau local, le proxy garde déjà un **pool** de connexions HTTP/1.1 ouvertes qui masque ce
425
+ blocage ; et côté Node, HTTP/2 ajoute du travail par requête (trames, compression HPACK des en-têtes,
426
+ contrôle de flux) que le parseur HTTP/1.1 n'a pas. Sur de petites requêtes, un serveur Node en HTTP/2 sert
427
+ souvent **moins** de requêtes par seconde qu'en HTTP/1.1. Le choix se fait sur la **forme du trafic** :
428
+
429
+ | Forme du trafic | h2c ? | Pourquoi |
430
+ | -------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
431
+ | gRPC | **requis** | gRPC n'existe qu'en HTTP/2. |
432
+ | Beaucoup de flux longs (SSE) par pod | **oui** | En HTTP/1.1, chaque flux occupe une connexion du proxy ; en h2c, ils partagent quelques connexions. |
433
+ | En-têtes lourds et répétés (cookies, jetons) | probable | HPACK les compresse d'une requête à l'autre. |
434
+ | REST classique, petites réponses | **mesurer** | Souvent neutre, parfois perdant. |
435
+ | WebSocket | **non** | Reste sur le port HTTP, en HTTP/1.1. |
436
+ | Plusieurs workers (`nodefony cluster -w N`) | **attention** | Le cluster Node répartit des **connexions**, pas des requêtes : avec les rares connexions h2c d'un proxy, un seul worker peut tout recevoir. Un process par pod n'a pas ce biais. |
437
+
438
+ **Ce que coûte h2c, mesuré** — production en un seul process, même route triviale, même instrument
439
+ (`h2load`), médiane de paires alternées ; débit de h2c rapporté à HTTP/1.1 sur la même machine, et le même
440
+ écart pour Node seul, sans framework :
441
+
442
+ | Forme (`h2load`) | Node seul | Nodefony |
443
+ | ----------------------------------------------- | --------- | -------- |
444
+ | 64 connexions × 1 flux — forme égale à HTTP/1.1 | −60 % | −48 % |
445
+ | 4 connexions × 16 flux — la forme d'un proxy | −14 % | −18 % |
446
+
447
+ L'écart est d'abord celui de HTTP/2 dans Node : à forme égale, Node seul perd davantage que Nodefony, dont
448
+ le travail par requête dilue le coût du protocole. En forme proxy, le multiplexage regagne presque tout, et
449
+ Nodefony reste dans la même zone. Sur une route triviale, h2c ne fait donc pas gagner de débit ; il se choisit
450
+ pour ce que le tableau ci-dessus nomme — gRPC, flux longs, en-têtes lourds.
451
+
452
+ **Mesurer avant de décider** : même pod, même route, en ne changeant que le protocole amont du proxy.
453
+ `wrk` ne parle que HTTP/1.1 ; `h2load` (nghttp2) parle les deux, ce qui garde le même instrument des deux
454
+ côtés. Comparer le débit, le p99 et le **CPU par requête** — côté pod **et** côté proxy, qui paie aussi
455
+ HPACK — en paires alternées, sur la médiane de plusieurs runs.
456
+
457
+ ```bash
458
+ # vérifier le port : la préface HTTP/2 directe, sans TLS
459
+ curl --http2-prior-knowledge http://127.0.0.1:5153/readyz
460
+ # même route, même instrument, deux protocoles
461
+ h2load --h1 -n 100000 -c 64 http://127.0.0.1:5151/readyz # HTTP/1.1
462
+ h2load -n 100000 -c 64 http://127.0.0.1:5153/readyz # h2c
463
+ ```
376
464
 
377
465
  ### `server-websocket` — WebSocket en clair
378
466
 
379
467
  Il ne crée **aucun** socket : il reçoit le `http.Server` déjà en écoute et s'y greffe
380
- (`server-websocket.ts:62`), puis relit `address()` pour connaître son port — il suit donc
468
+ (`server-websocket.ts#Websocket.server`), puis relit `address()` pour connaître son port — il suit donc
381
469
  automatiquement un éventuel décalage de port.
382
470
 
383
471
  - **Options transmises à `ws`** telles quelles (compression, validation UTF-8, `maxPayload`…), avec deux
384
472
  réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
385
- le battement de cœur (`server-websocket.ts:80`).
386
- - **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts:87`) et par connexion
387
- (`trackPong()`, `server-websocket.ts:113`).
473
+ le battement de cœur (`server-websocket.ts#Websocket.createServer`).
474
+ - **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts#Websocket.createServer`) et par connexion
475
+ (`trackPong()`, `http-kernel.ts#HttpKernel.handleWebsocket`, juste après l'acceptation du handshake).
388
476
  - **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
389
- (`prependOnceListener`, `server-websocket.ts:91`) — l'ordre
477
+ (`prependOnceListener`, `server-websocket.ts#Websocket.createServer`) — l'ordre
390
478
  compte, voir la section Arrêt gracieux.
391
479
 
392
480
  ### `server-websocket-secure` — WebSocket sur TLS
393
481
 
394
482
  Même code, une différence qui a son importance : il lit sa **propre** section de configuration,
395
- `websocketSecure`, et non `websocket` (`server-websocket-secure.ts:50`). Régler `websocket.maxPayload`
483
+ `websocketSecure`, et non `websocket` (`server-websocket-secure.ts#WebsocketSecure.constructor`). Régler `websocket.maxPayload`
396
484
  ne change donc rien au WSS ; les deux sections ont la même forme et les mêmes défauts.
397
485
 
398
486
  ### `server-static` — les fichiers, en repli
@@ -406,10 +494,10 @@ pour autant supprimer les montages programmatiques.
406
494
 
407
495
  C'est la distinction la plus utile de cette page, et celle qu'on rate le plus souvent.
408
496
 
409
- | Question | Où ça se règle | Source |
410
- | ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
411
- | **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts:149`) |
412
- | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:1171`) |
497
+ | Question | Où ça se règle | Source |
498
+ | ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------- |
499
+ | **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts#serversSchema`) |
500
+ | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts#httpConfigSchema`) |
413
501
 
414
502
  Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
415
503
  la CI et le cluster) ; le **réglage** est une propriété de l'application.
@@ -422,16 +510,17 @@ la CI et le cluster) ; le **réglage** est une propriété de l'application.
422
510
  | `servers.http` | `{ port }` \| `false` | `{ port: 5151 }` | Serveur en clair. `false` = TLS-only (le WS tombe avec). |
423
511
  | `servers.https` | `{ port, protocol }` \| `false` | `{ port: 5152, protocol: "2.0" }` | Serveur TLS. `false` = nominal cloud-native (le WSS tombe). |
424
512
  | `servers.https.protocol` | `"1.1"` \| `"2.0"` | `"2.0"` | HTTP/2 (h2) avec repli HTTP/1.1 par ALPN, ou HTTP/1.1 seul. |
513
+ | `servers.h2c` | `{ enabled, port }` | `{ enabled: false, port: 5153 }` | HTTP/2 en clair pour un proxy inverse — section `server-h2c`. |
425
514
  | `servers.statics` | bool | `true` | Monte le service de fichiers statiques. |
426
515
  | `servers.portPolicy` | `"auto"` \| `"strict"` | `auto` en dev, `strict` en prod **et** test | Que faire si le port est occupé (section suivante). |
427
516
  | `servers.portRetryAttempts` | int ≥ 0 | `20` | En `auto`, nombre de ports essayés après le port désiré. |
428
517
 
429
- Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaults.ts:34`).
518
+ Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaults.ts#defaultAppConfig`).
430
519
 
431
520
  ### Niveau 2 — le transport HTTP / HTTPS
432
521
 
433
- Table dérivée de `httpServerSchema` (`config.ts:272`) ; la section `https` reprend les mêmes clés et en
434
- ajoute une (`httpsServerSchema`, `config.ts:336`).
522
+ Table dérivée de `httpServerSchema` (`config.ts#httpServerSchema`) ; la section `https` reprend les mêmes clés et en
523
+ ajoute une (`httpsServerSchema`, `config.ts#httpsServerSchema`).
435
524
 
436
525
  | Option | Type | Défaut | Effet |
437
526
  | ---------------------------- | ----- | ------- | -------------------------------------------------------------------------------- |
@@ -450,8 +539,8 @@ quelle à Node. C'est délibéré — un schéma strict effacerait silencieuseme
450
539
 
451
540
  ### Niveau 2 — HTTP/2
452
541
 
453
- Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
454
- (`maxSessionMemory`, `server-https.ts:215`).
542
+ Depuis `http2Schema` (`config.ts#http2Schema`), appliqué seulement si défini
543
+ (`maxSessionMemory`, `server-https.ts#ServerHttps.createServerH2`).
455
544
 
456
545
  | Option | Type | Défaut | Effet |
457
546
  | ---------------------- | ---- | ------ | ----------------------------------------------------------------------------- |
@@ -460,22 +549,22 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
460
549
 
461
550
  ### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
462
551
 
463
- Depuis `websocketSchema` (`config.ts:535`). Les deux sections partagent la forme et les défauts ; le WSS
464
- lit `websocketSecure` (`config.ts:1240`).
465
-
466
- | Option | Type | Défaut | Effet |
467
- | ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------- |
468
- | `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
469
- | `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
470
- | `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket. |
471
- | `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts:561`). |
472
- | `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts:570`). |
473
- | `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
474
- | `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
475
- | `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
476
- | `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité). |
477
- | `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM. |
478
- | `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013). |
552
+ Depuis `websocketSchema` (`config.ts#websocketSchema`). Les deux sections partagent la forme et les défauts ; le WSS
553
+ lit `websocketSecure` (`config.ts#httpConfigSchema`).
554
+
555
+ | Option | Type | Défaut | Effet |
556
+ | ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------------- |
557
+ | `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
558
+ | `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
559
+ | `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket. |
560
+ | `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts#websocketSchema`). |
561
+ | `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts#websocketSchema`). |
562
+ | `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
563
+ | `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
564
+ | `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
565
+ | `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité). |
566
+ | `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM. |
567
+ | `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013). |
479
568
 
480
569
  > [!WARNING]
481
570
  > `keepalive*`, `closeTimeout`, `maxBackpressure` et `backpressurePolicy` sont des **réglages
@@ -493,7 +582,7 @@ panne, c'est une nuisance.
493
582
 
494
583
  ### La règle
495
584
 
496
- `resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:1845` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
585
+ `resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts#resolvePortPolicy` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
497
586
  toujours :
498
587
 
499
588
  | Environnement | Défaut | Pourquoi |
@@ -510,7 +599,7 @@ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
510
599
  servers.portPolicy = "strict".
511
600
  ```
512
601
 
513
- Le décalage est **toujours annoncé** (`server-http.ts:130`) : jamais de dégradation silencieuse. Trois
602
+ Le décalage est **toujours annoncé** (`server-http.ts#ServerHttp.createServer`) : jamais de dégradation silencieuse. Trois
514
603
  détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
515
604
 
516
605
  1. **Le repli se décide au `listen()`, qui est atomique.** Une seule sonde le précède, sur le port
@@ -518,24 +607,24 @@ détails d'implémentation valent d'être connus, parce qu'ils expliquent des co
518
607
  même à un `0.0.0.0` (`detectPortConflict`). Elle est **sautée dans un worker de cluster**
519
608
  (`cluster.isWorker`) : le port y est partagé par le maître, et le voisin qui répond est un frère,
520
609
  pas un intrus. La course résiduelle retombe sur `EADDRINUSE` (`bindWithFallback()`,
521
- `portBinder.ts:263`).
610
+ `portBinder.ts#bindWithFallback`).
522
611
  2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
523
612
  ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
524
- sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:105`, réservation `portBinder.ts:105`).
613
+ sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts#buildBindPlan`, réservation `portBinder.ts#buildBindPlan`).
525
614
  3. **Le gestionnaire d'erreur durable est posé APRÈS le bind.** Attaché avant, il verrait passer les
526
615
  `EADDRINUSE` de repli et terminerait le kernel en croyant à une panne
527
- (`ServerHttp.attachErrorHandler()`, `server-http.ts:199`).
616
+ (`ServerHttp.attachErrorHandler()`, `server-http.ts#ServerHttp.attachErrorHandler`).
528
617
 
529
618
  Quand le bind échoue pour de bon — `strict`, ou tous les replis épuisés, ou une erreur qui n'est pas un
530
619
  conflit de port — c'est **fatal** : message explicite puis terminaison du processus
531
- (`ServerHttp.reportBindError()`, `server-http.ts:212`). Un serveur qui n'écoute pas ne doit jamais
620
+ (`ServerHttp.reportBindError()`, `server-http.ts#ServerHttp.reportBindError`). Un serveur qui n'écoute pas ne doit jamais
532
621
  laisser un processus se croire démarré.
533
622
 
534
623
  ### Le corollaire : les ports effectifs sont publiés
535
624
 
536
625
  Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
537
626
  convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
538
- que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1279`) écrit donc la
627
+ que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts#HttpKernel.publishRuntimePorts`) écrit donc la
539
628
  topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
540
629
  environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
541
630
  aussi de la convention,
@@ -549,12 +638,12 @@ serveur qui, lui, écoute très bien.
549
638
  **Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
550
639
  n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
551
640
  ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
552
- (`Certificate.resolveStrategy()`, `certificates.ts:577`).
641
+ (`Certificate.resolveStrategy()`, `certificates.ts#Certificate.resolveStrategy`).
553
642
 
554
643
  ### Les quatre stratégies
555
644
 
556
- Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
557
- `Certificate.resolveStrategy()` (`certificates.ts:577`).
645
+ Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts#certificatesSchema`), résolues par
646
+ `Certificate.resolveStrategy()` (`certificates.ts#Certificate.resolveStrategy`).
558
647
 
559
648
  | Stratégie | Quand l'utiliser | Ce qui se passe |
560
649
  | --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
@@ -564,31 +653,29 @@ Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), r
564
653
  | `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé (`node:crypto`), non trusté. |
565
654
 
566
655
  ```typescript
567
- // nodefony.config.ts — production : certificat fourni, jamais généré
568
- export default defineConfig(() => ({
569
- modules: [
570
- use("@nodefony/http", {
571
- certificates: {
572
- strategy: "explicit",
573
- key: "/etc/tls/privkey.pem",
574
- cert: "/etc/tls/fullchain.pem",
575
- ca: "/etc/tls/chain.pem",
576
- },
577
- https: { rejectUnauthorized: true },
578
- }),
579
- "@nodefony/framework",
580
- ],
581
- }));
656
+ // nodefony/config/http.ts — production : certificat fourni, jamais généré
657
+ import type { IHttpConfigInput } from "@nodefony/http";
658
+
659
+ export const httpConfig = () =>
660
+ ({
661
+ certificates: {
662
+ strategy: "explicit",
663
+ key: "/etc/tls/privkey.pem",
664
+ cert: "/etc/tls/fullchain.pem",
665
+ ca: "/etc/tls/chain.pem",
666
+ },
667
+ https: { rejectUnauthorized: true },
668
+ }) satisfies IHttpConfigInput;
582
669
  ```
583
670
 
584
671
  > [!TIP]
585
672
  > Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
586
673
  > et au WSS) : `brew install mkcert nss && mkcert -install`. Nodefony le détecte tout seul, sinon il
587
- > l'annonce et retombe sur l'auto-signé (`certificates.ts:602`).
674
+ > l'annonce et retombe sur l'auto-signé (`certificates.ts#Certificate.resolveStrategy`).
588
675
 
589
676
  ### Ce que le chemin `explicit` évite
590
677
 
591
- Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330`). La génération de
678
+ Aucune génération : le certificat fourni est lu tel quel (`certificates.ts#trustOutboundAuthorities`). La génération de
592
679
  l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
593
680
  tierce n'est installée avec `@nodefony/http`.
594
681
 
@@ -612,28 +699,48 @@ use("@nodefony/http", {
612
699
 
613
700
  Le gabarit d'application la lit dans `NF_TLS_CA_FILE`. Les autorités sont ajoutées à celles du
614
701
  processus à l'enregistrement du module, avant tout service — donc aussi pour une commande en ligne
615
- (`Http.onKernelRegister()`, `index.ts:104` ; `trustCaFile()`, `certificates.ts:131`). Un fichier
702
+ (`Http.onKernelRegister()`, `index.ts#Http.onKernelRegister` ; `trustCaFile()`, `certificates.ts#trustCaFile`). Un fichier
616
703
  introuvable ou sans certificat **interrompt le démarrage** en nommant le chemin (code 78), plutôt
617
- qu'un `fetch failed` loin de sa cause. C'est l'équivalent de `NODE_EXTRA_CA_CERTS`, porté par la
704
+ qu'un `fetch failed` loin de sa cause — et, quand le chemin vient de `NF_TLS_CA_FILE`, le refus le
705
+ dit. Une seule exception : l'autorité de **développement** (`nodefony/config/certificates/ca/…`)
706
+ déclarée avant d'avoir été engendrée est **ignorée en développement**, une ligne du journal le dit ;
707
+ elle n'a encore rien signé, et le serveur de développement lui fait confiance dès qu'il la fabrique.
708
+ Rien n'est fabriqué à l'enregistrement : une commande console n'écrit aucun certificat. C'est l'équivalent de `NODE_EXTRA_CA_CERTS`, porté par la
618
709
  configuration : rien à poser au lancement, et la même écriture sous Windows.
619
710
 
711
+ Ce que chaque décor donne — le message du refus porte la même consigne
712
+ (`caFileRemedy()`, `certificates.ts`), et chaque ligne est éprouvée par
713
+ `tests/unit/certificates.test.ts` :
714
+
715
+ | Décor | Résultat |
716
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
717
+ | développement, autorité de dev pas encore engendrée | démarre ; une ligne INFO dit qu'elle naîtra au premier `npx nodefony development` |
718
+ | développement ou production, autorité présente | démarre ; l'autorité est reconnue pour les appels sortants |
719
+ | production, autorité de dev absente | refus 78 : l'engendrer une fois (`npx nodefony http:certificates`), ou retirer la déclaration |
720
+ | autorité d'entreprise absente (tout décor) | refus 78 : poser le fichier au chemin déclaré, ou retirer la déclaration |
721
+ | droit de lecture refusé | refus 78 : donner le droit au compte qui lance l'application (Kubernetes : ConfigMap/Secret lisible) |
722
+ | un dossier déclaré au lieu du fichier | refus 78 : désigner le fichier PEM qu'il contient |
723
+ | une clé privée, ou un certificat DER | refus 78 : fournir le CERTIFICAT en PEM (`openssl x509 -inform der -in a.der -out a.pem`) |
724
+
725
+ Quand le chemin vient de `NF_TLS_CA_FILE`, le refus le dit : c'est la ligne du `.env` à corriger.
726
+
620
727
  ### Conformité de l'auto-signé
621
728
 
622
729
  Un certificat de développement bâclé fait perdre des heures (« not yet valid », « common name
623
730
  invalid »). Celui de Nodefony respecte les règles qui comptent :
624
731
 
625
- | Exigence | Norme | Mise en œuvre |
626
- | --------------------------------------- | -------------------- | --------------------------------------------------------- |
627
- | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
628
- | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:443`) |
629
- | Le SAN fait foi, pas le CN | RFC 9525 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
630
- | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
631
- | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
732
+ | Exigence | Norme | Mise en œuvre |
733
+ | --------------------------------------- | -------------------- | ----------------------------------------------------------------------------------- |
734
+ | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts#selfSignedSchema`) |
735
+ | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts#Certificate.generateSerialHex`) |
736
+ | Le SAN fait foi, pas le CN | RFC 9525 | SAN dérivé du kernel si non fourni (`config.ts#selfSignedSchema`) |
737
+ | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts#selfSignedSchema`) |
738
+ | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts#certificatesSchema`) |
632
739
 
633
740
  ### Régénération automatique
634
741
 
635
742
  Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
636
- (`certificates.ts:743`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
743
+ (`certificates.ts#Certificate.isCertAdequate`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
637
744
  plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
638
745
  contrôle laisserait un certificat obsolète en place indéfiniment.
639
746
 
@@ -645,7 +752,7 @@ Dès qu'un proxy est devant l'application, trois questions se posent — et troi
645
752
 
646
753
  Sans barrière, n'importe quel client peut envoyer `X-Forwarded-For: 1.2.3.4` et usurper son IP :
647
754
  contournement de rate-limit, journaux d'audit falsifiés. Le défaut est donc **`false` — ces en-têtes
648
- sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réelle, non falsifiable.
755
+ sont ignorés** (`config.ts#healthSchema`), et l'IP retenue est celle de la socket réelle, non falsifiable.
649
756
 
650
757
  | Valeur | Sens |
651
758
  | -------------------------------------------- | ------------------------------------------------------------------ |
@@ -655,30 +762,30 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
655
762
  | `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
656
763
 
657
764
  La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
658
- `http-kernel.ts:627`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
765
+ `http-kernel.ts#HttpKernel.getTrustProxyChecker`, via `buildTrustProxy()`, `trustProxy.ts#buildTrustProxy`) : aucune structure allouée par
659
766
  requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
660
- (`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
767
+ (`resolveForwarded()`, `forwarded.ts#resolveForwarded`) — conforme RFC 7239 et à la recommandation OWASP.
661
768
 
662
769
  ### « Ce `Host` est-il le mien ? » → `trustedHosts`
663
770
 
664
771
  Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
665
772
  kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
666
- `http-kernel.ts:1082`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
773
+ `http-kernel.ts#HttpKernel.compileAlias`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
667
774
  d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
668
- filtre déjà le `Host` (`config.ts:974`).
775
+ filtre déjà le `Host` (`config.ts#healthSchema`).
669
776
 
670
777
  ### « Cette page a-t-elle le droit d'ouvrir un WebSocket ? » → `allowedOrigins`
671
778
 
672
779
  Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
673
780
  WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
674
781
  par défaut que l'`Origin` du handshake corresponde au `Host` servi
675
- (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:724`), avec tolérance loopback en développement
676
- et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
677
- Violation)**, jamais par un code HTTP.
782
+ (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts#HttpKernel.checkWebsocketOrigin`), avec tolérance loopback en développement
783
+ et allowlist explicite pour les SPA cross-origine. Un refus se solde par un **`403` au handshake**, avant le
784
+ `101` : la socket ne s'ouvre jamais (RFC 6455 §4.2.2).
678
785
 
679
786
  > [!NOTE]
680
787
  > Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
681
- > (`http-kernel.ts:642`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
788
+ > (`http-kernel.ts#HttpKernel.getWsOriginPolicy`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
682
789
  > il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
683
790
 
684
791
  ## Probes de santé — `/livez` et `/readyz`
@@ -695,13 +802,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
695
802
  protéger.
696
803
 
697
804
  Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
698
- (`http-kernel.ts:1093`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
699
- réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:561`). Et surtout : **avant le
805
+ (`http-kernel.ts#HttpKernel.onHttpRequest`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
806
+ réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts#HttpKernel.#respondHealth`). Et surtout : **avant le
700
807
  rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
701
808
 
702
809
  | Option | Type | Défaut | Effet |
703
810
  | --------------- | ------ | --------- | ---------------------------------------------------------------------- |
704
- | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:961`). |
811
+ | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts#healthSchema`). |
705
812
  | `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
706
813
  | `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
707
814
  | `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
@@ -765,28 +872,35 @@ sequenceDiagram
765
872
  ```
766
873
 
767
874
  **Étape 1 — la disponibilité bascule en premier.** L'écouteur est posé à `onPostReady` pour être
768
- inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:587`). `readyz` répond `503`, le
875
+ inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts#HttpKernel.init`). `readyz` répond `503`, le
769
876
  load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
770
877
  propager.
771
878
 
772
- **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:121`)
773
- envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
879
+ **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts#Websocket.terminate`)
880
+ envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts#Websocket.terminate`).
774
881
  Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
775
882
  indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
776
883
 
777
- **Étape 3 — les requêtes HTTP se terminent.** Le drain est délégué à `http-terminator`
778
- (`createDrainTerminator()`, `serverShutdown.ts:25`) : en-tête `connection: close` injecté sur les
779
- réponses en cours, sockets inactives fermées, destruction forcée au-delà de `shutdownTimeout` (défaut
780
- `5000` ms, `serverShutdown.ts:38`).
884
+ **Étape 3 — les requêtes HTTP se terminent.** Le drain (`createDrainTerminator()`,
885
+ `serverShutdown.ts#createDrainTerminator`) cesse d'accepter, puis traite chaque connexion selon son protocole :
886
+
887
+ - **HTTP/2** (5152, et h2c) : chaque session reçoit un **GOAWAY** — les flux en vol se terminent, les
888
+ nouveaux sont refusés, et le client (navigateur, proxy) rouvre ailleurs au lieu de constater une
889
+ coupure ;
890
+ - **HTTP/1.1** : en-tête `connection: close` injecté sur les réponses en cours, sockets inactives
891
+ fermées.
892
+
893
+ Ce qui reste au-delà de `shutdownTimeout` (défaut `5000` ms, `serverShutdown.ts#DEFAULT_SHUTDOWN_TIMEOUT`)
894
+ est détruit.
781
895
 
782
896
  > [!IMPORTANT]
783
897
  > L'ordre WS-avant-HTTP n'est pas cosmétique. Les serveurs WebSocket s'inscrivent en
784
- > `prependOnceListener` (`server-websocket.ts:91`), les serveurs HTTP en `once`
785
- > (`server-http.ts:170`). Inversé, le terminator détruirait les sockets déjà upgradées **sans** frame
898
+ > `prependOnceListener` (`server-websocket.ts#Websocket.createServer`), les serveurs HTTP en `once`
899
+ > (`server-http.ts#ServerHttp.createServer`). Inversé, le drain détruirait les sockets déjà upgradées **sans** frame
786
900
  > Close : retour du 1006 pour tous les clients temps réel.
787
901
 
788
902
  Deux garde-fous encadrent la séquence : `shutdownTimeout` **par serveur** (le drain nominal) et
789
- `shutdownDeadline` **global** au kernel (défaut 15 s, `src/nodefony/src/config/defaults.ts:46`), filet
903
+ `shutdownDeadline` **global** au kernel (défaut 15 s, `src/nodefony/src/config/defaults.ts#defaultAppConfig`), filet
790
904
  anti-écouteur bloqué. Les deux doivent rester sous la période de grâce de l'orchestrateur — 30 s en
791
905
  Kubernetes, 10 s avec Docker.
792
906
 
@@ -800,34 +914,34 @@ transport.
800
914
  Dès qu'un écouteur `clientError` est attaché, **Node cesse de fermer la socket automatiquement**. Un
801
915
  écouteur naïf (« je journalise et je passe ») transforme donc une requête malformée en **fuite de
802
916
  socket et de descripteur de fichier** — c'est-à-dire un vecteur de déni de service. Nodefony répond et
803
- ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les en-têtes débordent
804
- (RFC 6585 §5), `400` sinon (`clientError.ts:25`), et rien du tout si la socket est déjà morte.
917
+ ferme explicitement (`handleClientError()`, `clientError.ts#handleClientError`) : `431` si les en-têtes débordent
918
+ (RFC 6585 §5), `400` sinon (`clientError.ts#handleClientError`), et rien du tout si la socket est déjà morte.
805
919
 
806
920
  ### Connexions WebSocket zombies
807
921
 
808
922
  Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
809
923
  descripteur retenus pour personne. Le battement de cœur les réclame
810
- (`startHeartbeat()`, `wsHeartbeat.ts:124`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
811
- les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts:37`).
924
+ (`startHeartbeat()`, `wsHeartbeat.ts#startHeartbeat`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
925
+ les `keepaliveGracePeriod` ms, la socket est détruite (`terminate`, `wsHeartbeat.ts#IHeartbeatClient.terminate`).
812
926
 
813
927
  L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
814
928
  serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
815
- (`trackPong()`, `wsHeartbeat.ts:54`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
929
+ (`trackPong()`, `wsHeartbeat.ts#trackPong`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
816
930
  horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
817
- configuration pathologique (`tick`, `wsHeartbeat.ts:136`) ; et un timer `unref` qui ne retient jamais le
931
+ configuration pathologique (`tick`, `wsHeartbeat.ts#startHeartbeat`) ; et un timer `unref` qui ne retient jamais le
818
932
  processus à l'arrêt.
819
933
 
820
934
  ### Floods de connexions
821
935
 
822
936
  L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
823
937
  IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
824
- (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`). Le `101` étant déjà émis par `ws`, un `429`
825
- est impossible → la connexion est fermée en **1013 « Try Again Later »**
826
- (`websocketQuotaRefusal()`, `http-kernel.ts:2230`), sans
938
+ (`HttpKernel.onWebsocketUpgrade()`, `http-kernel.ts#HttpKernel.onWebsocketUpgrade`), et AVANT le `101` : le handshake reçoit un
939
+ **`429`** HTTP, jamais une socket ouverte puis fermée
940
+ (`websocketQuotaRefusal()`, `http-kernel.ts#HttpKernel.websocketQuotaRefusal`), sans
827
941
  journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
828
942
 
829
943
  Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
830
- `wsMaxConnectionsPerIp` (`config.ts:1250`). En cloud-native, laisser `null` et déléguer à l'edge —
944
+ `wsMaxConnectionsPerIp` (`config.ts#httpConfigSchema`). En cloud-native, laisser `null` et déléguer à l'edge —
831
945
  nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
832
946
  descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
833
947
 
@@ -844,17 +958,17 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
844
958
  par seconde. Les choix visibles dans le code :
845
959
 
846
960
  - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
847
- socket (`wsHeartbeat.ts:137`).
961
+ socket (`wsHeartbeat.ts#startHeartbeat`).
848
962
  - **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
849
- `trustedHosts` sont compilés une fois et mémoïsés (`getTrustProxyChecker()`, `http-kernel.ts:627` ; `getWsOriginPolicy()`, `http-kernel.ts:634` ; `compileAlias()`, `http-kernel.ts:1082`).
963
+ `trustedHosts` sont compilés une fois et mémoïsés (`getTrustProxyChecker()`, `http-kernel.ts#HttpKernel.getTrustProxyChecker` ; `getWsOriginPolicy()`, `http-kernel.ts#HttpKernel.getWsOriginPolicy` ; `compileAlias()`, `http-kernel.ts#HttpKernel.compileAlias`).
850
964
  - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
851
965
  DI et l'ALS : un flood coûte une recherche dans une table de hachage.
852
966
  - **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
853
- (`http-kernel.ts:388`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
967
+ (`http-kernel.ts#HttpKernel.constructor`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
854
968
  - **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
855
969
  route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
856
970
  production mono-processus).
857
- - **Aucune génération en production** avec un certificat fourni (`certificates.ts:330`).
971
+ - **Aucune génération en production** avec un certificat fourni (`certificates.ts#trustOutboundAuthorities`).
858
972
 
859
973
  Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
860
974
  boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
@@ -869,61 +983,61 @@ demande le backplane realtime.
869
983
 
870
984
  ## 📜 Normes appliquées
871
985
 
872
- | Domaine | Norme | Ancrage |
873
- | ------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
874
- | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1093`) |
875
- | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
876
- | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
877
- | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
878
- | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
879
- | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:121`) |
880
- | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:561`) |
881
- | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:641`) |
882
- | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:585`) |
883
- | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:724`) |
884
- | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
885
- | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:443`) |
886
- | Certificat — identité par le SAN | RFC 9525 | `sanSchema` (`config.ts:446`) |
986
+ | Domaine | Norme | Ancrage |
987
+ | ------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
988
+ | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts#HttpKernel.onHttpRequest`) |
989
+ | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts#ServerHttps.createServerH2`) |
990
+ | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts#http2Schema`) |
991
+ | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts#handleClientError`) |
992
+ | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts#certificatesSchema`) |
993
+ | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts#Websocket.terminate`) |
994
+ | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts#websocketSchema`) |
995
+ | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts#websocketSchema`) |
996
+ | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts#websocketSchema`) |
997
+ | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts#HttpKernel.checkWebsocketOrigin`) |
998
+ | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts#resolveForwarded`) |
999
+ | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts#Certificate.generateSerialHex`) |
1000
+ | Certificat — identité par le SAN | RFC 9525 | `sanSchema` (`config.ts#sanSchema`) |
887
1001
 
888
1002
  ## ⚠️ Pièges (symptôme → cause → correction)
889
1003
 
890
- | Symptôme | Cause | Correction |
891
- | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
892
- | 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 |
893
- | 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é |
894
- | `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:220`) |
895
- | Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
896
- | 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 |
897
- | 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`) |
898
- | 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 |
899
- | 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 |
900
- | 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:860`) |
901
- | `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 |
902
- | Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
903
- | 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:543`) |
904
- | `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux |
905
- | Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
906
- | Handshake WebSocket refusé en `1008` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
907
- | Fermetures WebSocket en `1013` inexpliquées | Rate-limit d'IP, ou plafond de connexions concurrentes | Vérifier `rateLimit` et `wsMaxConnectionsPerIp` |
1004
+ | Symptôme | Cause | Correction |
1005
+ | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
1006
+ | 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 |
1007
+ | 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é |
1008
+ | `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#RouteMetaData`) |
1009
+ | Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts#WebsocketSecure.constructor`) |
1010
+ | 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 |
1011
+ | 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#Websocket.terminate`) |
1012
+ | 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 |
1013
+ | 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 |
1014
+ | 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#HttpKernel.handle`) |
1015
+ | `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 |
1016
+ | Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
1017
+ | 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#Certificate.isDev`) |
1018
+ | `strategy: "explicit"` fait échouer le boot | `key`/`cert` absents de la configuration | Fournir les deux chemins — l'échec est volontaire, jamais un repli silencieux |
1019
+ | Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
1020
+ | Handshake WebSocket refusé en `403` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
1021
+ | Handshakes WebSocket refusés en `429` | Rate-limit d'IP, ou plafond de connexions concurrentes | Vérifier `rateLimit` et `wsMaxConnectionsPerIp` |
908
1022
 
909
1023
  ## 📡 Observabilité — Studio et CLI
910
1024
 
911
1025
  **Studio.** L'onglet **Configuration** rend la config du module en réglages documentés (type, défaut,
912
1026
  état, valeur effective) à partir du JSON Schema dérivé du schéma Zod
913
- (`src/packages/@nodefony/http/index.ts:79`). Le profiler expose les phases par requête, y compris
1027
+ (`src/packages/@nodefony/http/index.ts#Http.constructor`). Le profiler expose les phases par requête, y compris
914
1028
  l'origine du transport.
915
1029
 
916
1030
  **CLI.** Trois commandes touchent directement aux serveurs :
917
1031
 
918
- | Commande | Rôle |
919
- | ---------------------------------------------------- | ------------------------------------------------------------------------ |
920
- | `nodefony http:network [interface] [--json]` | Interfaces réseau vues par le kernel (`networkCommand.ts:18`). |
921
- | `nodefony http:certificates [--force] [--json]` | (Re)génère et décrit le certificat de dev (`certificatesCommand.ts:24`). |
922
- | `nodefony proxy:generate <nginx\|haproxy> [-o file]` | Dérive une configuration reverse-proxy de l'introspection réelle. |
1032
+ | Commande | Rôle |
1033
+ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
1034
+ | `nodefony http:network [interface] [--json]` | Interfaces réseau vues par le kernel (`networkCommand.ts#Network.constructor`). |
1035
+ | `nodefony http:certificates [--force] [--json]` | (Re)génère et décrit le certificat de dev (`certificatesCommand.ts#Certificates.constructor`). |
1036
+ | `nodefony proxy:generate <nginx\|haproxy> [-o file]` | Dérive une configuration reverse-proxy de l'introspection réelle. |
923
1037
 
924
1038
  `proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
925
1039
  confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
926
- résumé de certificat vient de `Certificate.describe()` (`certificates.ts:973`), source unique partagée
1040
+ résumé de certificat vient de `Certificate.describe()` (`certificates.ts#Certificate.describe`), source unique partagée
927
1041
  par la commande, le boot et un futur écran d'administration.
928
1042
 
929
1043
  **Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils