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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +5 -3
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +11 -5
  6. package/dist/nodefony/command/certificatesCommand.js +0 -1
  7. package/dist/nodefony/config/config.js +48 -7
  8. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  9. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  10. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  11. package/dist/nodefony/service/audit-logger.js +2 -3
  12. package/dist/nodefony/service/certificates.js +214 -158
  13. package/dist/nodefony/service/error-renderer.js +1 -1
  14. package/dist/nodefony/service/http-kernel.js +221 -47
  15. package/dist/nodefony/service/reverse-proxy.js +336 -0
  16. package/dist/nodefony/service/servers/server-http.js +3 -3
  17. package/dist/nodefony/service/servers/server-https.js +3 -3
  18. package/dist/nodefony/service/servers/server-static.js +4 -7
  19. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  20. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  21. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  22. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  23. package/dist/nodefony/service/trace.js +2 -1
  24. package/dist/nodefony/service/upload/upload-service.js +3 -3
  25. package/dist/nodefony/service/x509.js +324 -0
  26. package/dist/nodefony/src/context/Context.js +28 -1
  27. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  28. package/dist/nodefony/src/context/forwarded.js +2 -2
  29. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  30. package/dist/nodefony/src/context/http/Request.js +9 -4
  31. package/dist/nodefony/src/context/http/Response.js +46 -7
  32. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
  34. package/dist/nodefony/src/context/responseEnded.js +27 -0
  35. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  36. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  37. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  38. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  39. package/dist/nodefony/src/proxy/forward.js +476 -0
  40. package/dist/nodefony/src/proxy/rules.js +182 -0
  41. package/dist/nodefony/src/servers/portBinder.js +57 -2
  42. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  43. package/dist/nodefony/src/session/session.js +28 -12
  44. package/dist/types/index.d.ts +5 -0
  45. package/dist/types/nodefony/config/config.d.ts +17 -0
  46. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  47. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  48. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  49. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  50. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  52. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  53. package/dist/types/nodefony/service/certificates.d.ts +126 -50
  54. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  55. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  56. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  57. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  58. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  59. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  60. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  61. package/dist/types/nodefony/service/x509.d.ts +86 -0
  62. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  63. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  64. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  65. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  66. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  67. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  68. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  69. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  70. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  71. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  72. package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
  73. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  74. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  75. package/docs/cookies.md +13 -13
  76. package/docs/index.md +9 -3
  77. package/docs/observabilite.md +18 -18
  78. package/docs/rate-limit.md +30 -30
  79. package/docs/reverse-proxy.md +522 -0
  80. package/docs/servers.md +115 -78
  81. package/docs/session.md +61 -61
  82. package/docs/sse.md +283 -0
  83. package/docs/upload.md +37 -37
  84. package/package.json +10 -13
package/docs/servers.md CHANGED
@@ -85,7 +85,7 @@ Trois idées à retenir, et tout le reste en découle :
85
85
  | Drain | Vidange : laisser finir les requêtes en cours avant de fermer, plutôt que couper les sockets. |
86
86
  | Liveness / Readiness | Sondes cloud : « le processus est-il vivant ? » / « peut-il recevoir du trafic ? ». |
87
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). |
88
+ | SAN | _Subject Alternative Name_ : la liste des noms/IP qu'un certificat couvre réellement (RFC 9525). |
89
89
  | mkcert | Outil de développement qui installe une autorité de certification locale **de confiance** sur la machine. |
90
90
  | Reverse-proxy / edge | Le nginx / HAProxy / ingress placé devant l'application, souvent porteur du TLS. |
91
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. |
@@ -128,12 +128,12 @@ Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttp
128
128
 
129
129
  **Le WebSocket n'est jamais un citoyen de seconde zone.** Il est adossé au serveur HTTP porteur
130
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:1619`) —, hérite de la même
131
+ _est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`) —, 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:1050`). Elle ne fait rien aujourd'hui.
136
+ > marquée `reserved` (`config.ts:1216`). Elle ne fait rien aujourd'hui.
137
137
 
138
138
  ## 🚀 Démarrage rapide
139
139
 
@@ -249,22 +249,31 @@ export default PingController;
249
249
 
250
250
  ### 4. Ce qu'on observe au boot
251
251
 
252
- Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1274`), 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`) :
252
+ Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1355`). En développement, le
253
+ bilan de démarrage (`renderStartupHuman`, `startupScreen.ts`) dit d'abord **où aller**,
254
+ puis **ce qui est à regarder**, puis les adresses d'écoute :
255
255
 
256
256
  ```text
257
- ✓ Prêt en 1.4s
257
+ ✓ Prêt en 6,1 s — Nodefony 10.0.0 · development
258
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
259
+ OUVRIR
260
+ ➜ Application https://localhost:5152/
261
+ ➜ Studio https://localhost:5152/nodefony
262
+
263
+ ÉTAT
264
+ Mode development · rechargement auto
265
+ Écoute HTTP + WS 127.0.0.1:5151 · HTTP/2 + WSS 127.0.0.1:5152
264
266
  ```
265
267
 
266
- Hors écran animé (production, CI, `--debug`), ce sont les bannières par serveur qui sortent
267
- (`ServerHttp.showBanner()`, `server-http.ts:242`, appelées par le kernel — `Kernel.ts:561`) :
268
+ 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
+ une IP comme domaine, et un cookie posé sous un nom ne part pas sous l'autre. Hors d'un
271
+ terminal, le même bilan sort ligne à ligne (`open.app: https://localhost:5152/`,
272
+ `listen.https: 127.0.0.1:5152`) ; pour une machine, `nodefony status --json` le rend en entier.
273
+
274
+ 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`) :
268
277
 
269
278
  ```text
270
279
  Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
@@ -327,8 +336,8 @@ Choisir en cinq secondes :
327
336
  | `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
328
337
  | `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
329
338
 
330
- L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1065`) : chaque serveur est
331
- consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:1062`).
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`).
332
341
  Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
333
342
 
334
343
  ### `server-http` — HTTP/1.1 en clair
@@ -375,7 +384,7 @@ automatiquement un éventuel décalage de port.
375
384
  réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
376
385
  le battement de cœur (`server-websocket.ts:80`).
377
386
  - **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts:87`) et par connexion
378
- (`trackPong()`, `server-websocket.ts:103`).
387
+ (`trackPong()`, `server-websocket.ts:113`).
379
388
  - **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
380
389
  (`prependOnceListener`, `server-websocket.ts:91`) — l'ordre
381
390
  compte, voir la section Arrêt gracieux.
@@ -400,7 +409,7 @@ C'est la distinction la plus utile de cette page, et celle qu'on rate le plus so
400
409
  | Question | Où ça se règle | Source |
401
410
  | ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
402
411
  | **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts:149`) |
403
- | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:989`) |
412
+ | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:1171`) |
404
413
 
405
414
  Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
406
415
  la CI et le cluster) ; le **réglage** est une propriété de l'application.
@@ -451,16 +460,16 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
451
460
 
452
461
  ### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
453
462
 
454
- Depuis `websocketSchema` (`config.ts:517`). Les deux sections partagent la forme et les défauts ; le WSS
455
- lit `websocketSecure` (`config.ts:1058`).
463
+ Depuis `websocketSchema` (`config.ts:535`). Les deux sections partagent la forme et les défauts ; le WSS
464
+ lit `websocketSecure` (`config.ts:1240`).
456
465
 
457
466
  | Option | Type | Défaut | Effet |
458
467
  | ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------- |
459
468
  | `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
460
469
  | `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
461
470
  | `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:543`). |
463
- | `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts:552`). |
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`). |
464
473
  | `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
465
474
  | `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
466
475
  | `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
@@ -484,7 +493,7 @@ panne, c'est une nuisance.
484
493
 
485
494
  ### La règle
486
495
 
487
- `resolvePortPolicy()` (cœur, `src/nodefony/src/service/dev/devProcess.ts:1730` — la même règle sert `nodefony doctor`) tranche selon l'environnement, et la valeur explicite gagne
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
488
497
  toujours :
489
498
 
490
499
  | Environnement | Défaut | Pourquoi |
@@ -504,9 +513,11 @@ WARNING Port 5151 déjà occupé → HTTP écoute sur 5153.
504
513
  Le décalage est **toujours annoncé** (`server-http.ts:130`) : jamais de dégradation silencieuse. Trois
505
514
  détails d'implémentation valent d'être connus, parce qu'ils expliquent des comportements surprenants.
506
515
 
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()`,
516
+ 1. **Le repli se décide au `listen()`, qui est atomique.** Une seule sonde le précède, sur le port
517
+ désiré : elle détecte un serveur déjà présent sur la boucle locale, que le noyau accorderait quand
518
+ même à un `0.0.0.0` (`detectPortConflict`). Elle est **sautée dans un worker de cluster**
519
+ (`cluster.isWorker`) : le port y est partagé par le maître, et le voisin qui répond est un frère,
520
+ pas un intrus. La course résiduelle retombe sur `EADDRINUSE` (`bindWithFallback()`,
510
521
  `portBinder.ts:263`).
511
522
  2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
512
523
  ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
@@ -524,9 +535,10 @@ laisser un processus se croire démarré.
524
535
 
525
536
  Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
526
537
  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
538
+ que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1279`) écrit donc la
528
539
  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,
540
+ environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
541
+ aussi de la convention,
530
542
  même en `strict`. L'écriture est au mieux-effort — une image en lecture seule ne fait jamais tomber un
531
543
  serveur qui, lui, écoute très bien.
532
544
 
@@ -537,19 +549,19 @@ serveur qui, lui, écoute très bien.
537
549
  **Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
538
550
  n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
539
551
  ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
540
- (`Certificate.resolveStrategy()`, `certificates.ts:407`).
552
+ (`Certificate.resolveStrategy()`, `certificates.ts:577`).
541
553
 
542
554
  ### Les quatre stratégies
543
555
 
544
- Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:475`), résolues par
545
- `Certificate.resolveStrategy()` (`certificates.ts:407`).
556
+ Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
557
+ `Certificate.resolveStrategy()` (`certificates.ts:577`).
546
558
 
547
559
  | Stratégie | Quand l'utiliser | Ce qui se passe |
548
560
  | --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
549
561
  | `auto` (défaut) | On ne veut pas décider | `key`+`cert` fournis → `explicit` ; sinon mkcert en dev ; sinon auto-signé. |
550
562
  | `explicit` | **Production** | Charge `key`/`cert`/`ca` depuis la config. Erreur au boot si absents. |
551
563
  | `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é. |
564
+ | `selfsigned` | Secours, CI, machine sans mkcert | Auto-signé (`node:crypto`), non trusté. |
553
565
 
554
566
  ```typescript
555
567
  // nodefony.config.ts — production : certificat fourni, jamais généré
@@ -572,13 +584,38 @@ export default defineConfig(() => ({
572
584
  > [!TIP]
573
585
  > Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
574
586
  > 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:401`).
587
+ > l'annonce et retombe sur l'auto-signé (`certificates.ts:602`).
576
588
 
577
589
  ### Ce que le chemin `explicit` évite
578
590
 
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:238`) : en production avec un certificat
581
- fourni, elle n'entre jamais dans le processus (`certificates.ts:238`).
591
+ Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330`). La génération de
592
+ l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
593
+ tierce n'est installée avec `@nodefony/http`.
594
+
595
+ ### Autorités sortantes — `extraCaFiles`
596
+
597
+ Les certificats ci-dessus sont ceux que l'application **présente**. L'inverse compte autant :
598
+ quand l'application **appelle** un autre service en `https` (Keycloak, une API partenaire, l'amont
599
+ du proxy inverse), elle n'accepte que les certificats signés par une autorité qu'elle connaît. Une
600
+ autorité publique ne demande rien, et celle de développement de Nodefony est reconnue d'office ;
601
+ une autorité **d'entreprise** se déclare :
602
+
603
+ ```typescript ignore
604
+ use("@nodefony/http", {
605
+ certificates: {
606
+ // Fichiers PEM, absolus ou relatifs à la racine de l'application.
607
+ // Un fichier peut porter une chaîne entière (racine + intermédiaires).
608
+ extraCaFiles: ["certs/autorite-entreprise.pem"],
609
+ },
610
+ });
611
+ ```
612
+
613
+ Le gabarit d'application la lit dans `NF_TLS_CA_FILE`. Les autorités sont ajoutées à celles du
614
+ 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
616
+ 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
618
+ configuration : rien à poser au lancement, et la même écriture sous Windows.
582
619
 
583
620
  ### Conformité de l'auto-signé
584
621
 
@@ -588,15 +625,15 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
588
625
  | Exigence | Norme | Mise en œuvre |
589
626
  | --------------------------------------- | -------------------- | --------------------------------------------------------- |
590
627
  | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
591
- | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:302`) |
592
- | Le SAN fait foi, pas le CN | RFC 6125 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
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`) |
593
630
  | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
594
631
  | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
595
632
 
596
633
  ### Régénération automatique
597
634
 
598
635
  Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
599
- (`certificates.ts:579`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
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
600
637
  plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
601
638
  contrôle laisserait un certificat obsolète en place indéfiniment.
602
639
 
@@ -618,7 +655,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
618
655
  | `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
619
656
 
620
657
  La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
621
- `http-kernel.ts:562`, via `buildTrustProxy()`, `trustProxy.ts:100`) : aucune structure allouée par
658
+ `http-kernel.ts:627`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
622
659
  requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
623
660
  (`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
624
661
 
@@ -626,7 +663,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
626
663
 
627
664
  Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
628
665
  kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
629
- `http-kernel.ts:964`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
666
+ `http-kernel.ts:1082`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
630
667
  d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
631
668
  filtre déjà le `Host` (`config.ts:974`).
632
669
 
@@ -635,13 +672,13 @@ filtre déjà le `Host` (`config.ts:974`).
635
672
  Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
636
673
  WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
637
674
  par défaut que l'`Origin` du handshake corresponde au `Host` servi
638
- (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:621`), avec tolérance loopback en développement
675
+ (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:724`), avec tolérance loopback en développement
639
676
  et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
640
677
  Violation)**, jamais par un code HTTP.
641
678
 
642
679
  > [!NOTE]
643
680
  > Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
644
- > (`http-kernel.ts:630`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
681
+ > (`http-kernel.ts:642`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
645
682
  > il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
646
683
 
647
684
  ## Probes de santé — `/livez` et `/readyz`
@@ -658,13 +695,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
658
695
  protéger.
659
696
 
660
697
  Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
661
- (`http-kernel.ts:972`) — 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:497`). Et surtout : **avant le
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
663
700
  rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
664
701
 
665
702
  | Option | Type | Défaut | Effet |
666
703
  | --------------- | ------ | --------- | ---------------------------------------------------------------------- |
667
- | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:940`). |
704
+ | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:961`). |
668
705
  | `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
669
706
  | `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
670
707
  | `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
@@ -728,11 +765,11 @@ sequenceDiagram
728
765
  ```
729
766
 
730
767
  **É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
768
+ inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:587`). `readyz` répond `503`, le
732
769
  load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
733
770
  propager.
734
771
 
735
- **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:113`)
772
+ **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:121`)
736
773
  envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
737
774
  Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
738
775
  indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
@@ -770,27 +807,27 @@ ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les
770
807
 
771
808
  Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
772
809
  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`).
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`).
775
812
 
776
813
  L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
777
814
  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
815
+ (`trackPong()`, `wsHeartbeat.ts:54`), donc zéro allocation par tick et aucun nettoyage à prévoir (les
779
816
  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
817
+ configuration pathologique (`tick`, `wsHeartbeat.ts:136`) ; et un timer `unref` qui ne retient jamais le
781
818
  processus à l'arrêt.
782
819
 
783
820
  ### Floods de connexions
784
821
 
785
822
  L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
786
823
  IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
787
- (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1619`). Le `101` étant déjà émis par `ws`, un `429`
824
+ (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1964`). Le `101` étant déjà émis par `ws`, un `429`
788
825
  est impossible → la connexion est fermée en **1013 « Try Again Later »**
789
- (`rateLimiter`, `http-kernel.ts:1644`), sans
826
+ (`websocketQuotaRefusal()`, `http-kernel.ts:2230`), sans
790
827
  journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
791
828
 
792
829
  Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
793
- `wsMaxConnectionsPerIp` (`config.ts:1067`). En cloud-native, laisser `null` et déléguer à l'edge —
830
+ `wsMaxConnectionsPerIp` (`config.ts:1250`). En cloud-native, laisser `null` et déléguer à l'edge —
794
831
  nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
795
832
  descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
796
833
 
@@ -807,17 +844,17 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
807
844
  par seconde. Les choix visibles dans le code :
808
845
 
809
846
  - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
810
- socket (`wsHeartbeat.ts:69`).
847
+ socket (`wsHeartbeat.ts:137`).
811
848
  - **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:407`, `http-kernel.ts:407`).
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`).
813
850
  - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
814
851
  DI et l'ALS : un flood coûte une recherche dans une table de hachage.
815
852
  - **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.
853
+ (`http-kernel.ts:388`). Un kubelet qui sonde toutes les 2 secondes ne pèse rien.
817
854
  - **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
818
855
  route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
819
856
  production mono-processus).
820
- - **`node-forge` jamais chargé en production** avec un certificat fourni (`certificates.ts:227`).
857
+ - **Aucune génération en production** avec un certificat fourni (`certificates.ts:330`).
821
858
 
822
859
  Ordre de grandeur mesuré : un processus Node saturé sur un cœur tient environ 400 requêtes/s en
823
860
  boucle locale avec dégradation gracieuse (1600 connexions concurrentes, aucun crash) ; côté WebSocket,
@@ -832,21 +869,21 @@ demande le backplane realtime.
832
869
 
833
870
  ## 📜 Normes appliquées
834
871
 
835
- | Domaine | Norme | Ancrage |
836
- | ------------------------------------- | ------------------ | -------------------------------------------------------------------------- |
837
- | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:972`) |
838
- | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
839
- | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
840
- | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
841
- | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
842
- | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:113`) |
843
- | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`) |
844
- | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`) |
845
- | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`) |
846
- | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:621`) |
847
- | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
848
- | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:302`) |
849
- | Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:442`) |
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`) |
850
887
 
851
888
  ## ⚠️ Pièges (symptôme → cause → correction)
852
889
 
@@ -854,16 +891,16 @@ demande le backplane realtime.
854
891
  | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
855
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 |
856
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é |
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`) |
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`) |
858
895
  | Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
859
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 |
860
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`) |
861
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 |
862
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 |
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`) |
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`) |
864
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 |
865
902
  | 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:555`) |
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`) |
867
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 |
868
905
  | Une IP falsifiée passe dans les journaux d'audit | `trustProxy` accordé trop largement | Restreindre à l'IP/CIDR du proxy, ou revenir à `false` |
869
906
  | Handshake WebSocket refusé en `1008` | `Origin` non autorisée (anti-CSWSH) | Ajouter l'origine dans `websocket.allowedOrigins` |
@@ -886,7 +923,7 @@ l'origine du transport.
886
923
 
887
924
  `proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
888
925
  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:862`), source unique partagée
926
+ résumé de certificat vient de `Certificate.describe()` (`certificates.ts:973`), source unique partagée
890
927
  par la commande, le boot et un futur écran d'administration.
891
928
 
892
929
  **Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils