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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +11 -5
  5. package/dist/nodefony/config/config.js +44 -4
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  7. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  8. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  9. package/dist/nodefony/service/audit-logger.js +2 -3
  10. package/dist/nodefony/service/certificates.js +140 -15
  11. package/dist/nodefony/service/error-renderer.js +1 -1
  12. package/dist/nodefony/service/http-kernel.js +221 -47
  13. package/dist/nodefony/service/reverse-proxy.js +336 -0
  14. package/dist/nodefony/service/servers/server-http.js +3 -3
  15. package/dist/nodefony/service/servers/server-https.js +3 -3
  16. package/dist/nodefony/service/servers/server-static.js +4 -7
  17. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  18. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  19. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  20. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  21. package/dist/nodefony/service/upload/upload-service.js +3 -3
  22. package/dist/nodefony/src/context/Context.js +28 -1
  23. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  24. package/dist/nodefony/src/context/forwarded.js +2 -2
  25. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  26. package/dist/nodefony/src/context/http/Request.js +1 -9
  27. package/dist/nodefony/src/context/http/Response.js +16 -1
  28. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  29. package/dist/nodefony/src/context/responseEnded.js +27 -0
  30. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  31. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  32. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  33. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  34. package/dist/nodefony/src/proxy/forward.js +476 -0
  35. package/dist/nodefony/src/proxy/rules.js +182 -0
  36. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  37. package/dist/nodefony/src/session/session.js +28 -12
  38. package/dist/types/index.d.ts +5 -0
  39. package/dist/types/nodefony/config/config.d.ts +16 -0
  40. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  41. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  42. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  43. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  44. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  45. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  46. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  47. package/dist/types/nodefony/service/certificates.d.ts +96 -5
  48. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  49. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  50. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  51. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  52. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  53. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  54. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  55. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  56. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  57. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  58. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  59. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  60. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  61. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  62. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  63. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  64. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  65. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  66. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  67. package/docs/cookies.md +13 -13
  68. package/docs/index.md +5 -1
  69. package/docs/observabilite.md +15 -15
  70. package/docs/rate-limit.md +30 -30
  71. package/docs/reverse-proxy.md +522 -0
  72. package/docs/servers.md +94 -60
  73. package/docs/session.md +61 -61
  74. package/docs/sse.md +283 -0
  75. package/docs/upload.md +23 -23
  76. package/package.json +8 -8
package/docs/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:1848`) —, 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:1128`) : chaque serveur est
331
- consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:1139`).
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:993`) |
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:521`). 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:1767` — 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 |
@@ -526,7 +535,7 @@ laisser un processus se croire démarré.
526
535
 
527
536
  Si le port peut glisser, alors « le serveur écoute sur 5151 » n'est plus une vérité mais une
528
537
  convention — et `nodefony status`, `nodefony stop` ou l'attente de disponibilité sonderaient un port
529
- que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1195`) écrit donc la
538
+ que personne n'écoute. `HttpKernel.publishRuntimePorts()` (`http-kernel.ts:1279`) écrit donc la
530
539
  topologie réelle (pid, ports obtenus, ports désirés) dans un fichier d'état, **dans tous les
531
540
  environnements** : une application qui déclare son port (`NF_PORT`, `NF_PORT_HTTPS` ou `servers.http.port`) sort
532
541
  aussi de la convention,
@@ -540,12 +549,12 @@ serveur qui, lui, écoute très bien.
540
549
  **Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
541
550
  n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
542
551
  ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
543
- (`Certificate.resolveStrategy()`, `certificates.ts:369`).
552
+ (`Certificate.resolveStrategy()`, `certificates.ts:577`).
544
553
 
545
554
  ### Les quatre stratégies
546
555
 
547
556
  Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:479`), résolues par
548
- `Certificate.resolveStrategy()` (`certificates.ts:369`).
557
+ `Certificate.resolveStrategy()` (`certificates.ts:577`).
549
558
 
550
559
  | Stratégie | Quand l'utiliser | Ce qui se passe |
551
560
  | --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
@@ -575,7 +584,7 @@ export default defineConfig(() => ({
575
584
  > [!TIP]
576
585
  > Pour un HTTPS de développement **sans avertissement navigateur** (indispensable au HMR cross-origin
577
586
  > et au WSS) : `brew install mkcert nss && mkcert -install`. Nodefony le détecte tout seul, sinon il
578
- > l'annonce et retombe sur l'auto-signé (`certificates.ts:398`).
587
+ > l'annonce et retombe sur l'auto-signé (`certificates.ts:602`).
579
588
 
580
589
  ### Ce que le chemin `explicit` évite
581
590
 
@@ -583,6 +592,31 @@ Aucune génération : le certificat fourni est lu tel quel (`certificates.ts:330
583
592
  l'auto-signé, elle, n'emploie que `node:crypto` (`x509.ts`) — aucune dépendance cryptographique
584
593
  tierce n'est installée avec `@nodefony/http`.
585
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.
619
+
586
620
  ### Conformité de l'auto-signé
587
621
 
588
622
  Un certificat de développement bâclé fait perdre des heures (« not yet valid », « common name
@@ -591,15 +625,15 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
591
625
  | Exigence | Norme | Mise en œuvre |
592
626
  | --------------------------------------- | -------------------- | --------------------------------------------------------- |
593
627
  | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
594
- | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:263`) |
595
- | 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`) |
596
630
  | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
597
631
  | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
598
632
 
599
633
  ### Régénération automatique
600
634
 
601
635
  Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
602
- (`certificates.ts:535`) 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
603
637
  plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
604
638
  contrôle laisserait un certificat obsolète en place indéfiniment.
605
639
 
@@ -621,7 +655,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
621
655
  | `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
622
656
 
623
657
  La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
624
- `http-kernel.ts:573`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
658
+ `http-kernel.ts:627`, via `buildTrustProxy()`, `trustProxy.ts:103`) : aucune structure allouée par
625
659
  requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
626
660
  (`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
627
661
 
@@ -629,7 +663,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
629
663
 
630
664
  Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
631
665
  kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
632
- `http-kernel.ts:987`). `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
633
667
  d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
634
668
  filtre déjà le `Host` (`config.ts:974`).
635
669
 
@@ -638,13 +672,13 @@ filtre déjà le `Host` (`config.ts:974`).
638
672
  Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
639
673
  WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
640
674
  par défaut que l'`Origin` du handshake corresponde au `Host` servi
641
- (`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
642
676
  et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
643
677
  Violation)**, jamais par un code HTTP.
644
678
 
645
679
  > [!NOTE]
646
680
  > Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
647
- > (`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,
648
682
  > il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
649
683
 
650
684
  ## Probes de santé — `/livez` et `/readyz`
@@ -661,13 +695,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
661
695
  protéger.
662
696
 
663
697
  Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
664
- (`http-kernel.ts:1009`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
665
- réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:508`). 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
666
700
  rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
667
701
 
668
702
  | Option | Type | Défaut | Effet |
669
703
  | --------------- | ------ | --------- | ---------------------------------------------------------------------- |
670
- | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:944`). |
704
+ | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:961`). |
671
705
  | `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
672
706
  | `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
673
707
  | `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
@@ -731,11 +765,11 @@ sequenceDiagram
731
765
  ```
732
766
 
733
767
  **Étape 1 — la disponibilité bascule en premier.** L'écouteur est posé à `onPostReady` pour être
734
- inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:550`). `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
735
769
  load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
736
770
  propager.
737
771
 
738
- **É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`)
739
773
  envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
740
774
  Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
741
775
  indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
@@ -773,27 +807,27 @@ ferme explicitement (`handleClientError()`, `clientError.ts:15`) : `431` si les
773
807
 
774
808
  Une socket dont le pair a disparu sans frame Close reste « ouverte » côté TCP : slot mémoire et
775
809
  descripteur retenus pour personne. Le battement de cœur les réclame
776
- (`startHeartbeat()`, `wsHeartbeat.ts:69`) : un ping toutes les `keepaliveInterval` ms ; sans pong dans
777
- 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`).
778
812
 
779
813
  L'implémentation est délibérément frugale — c'est du chemin chaud : **un seul `setInterval` par
780
814
  serveur**, jamais un timer par connexion ; deux horodatages posés directement sur la socket
781
- (`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
782
816
  horodatages meurent avec la socket) ; une granularité de réveil plancher à 250 ms pour borner une
783
- 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
784
818
  processus à l'arrêt.
785
819
 
786
820
  ### Floods de connexions
787
821
 
788
822
  L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
789
823
  IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
790
- (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1848`). 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`
791
825
  est impossible → la connexion est fermée en **1013 « Try Again Later »**
792
- (`rateLimiter`, `http-kernel.ts:1863`), sans
826
+ (`websocketQuotaRefusal()`, `http-kernel.ts:2230`), sans
793
827
  journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
794
828
 
795
829
  Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
796
- `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 —
797
831
  nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
798
832
  descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
799
833
 
@@ -810,13 +844,13 @@ Le transport est du **chemin chaud absolu** : ce qui coûte ici est multiplié p
810
844
  par seconde. Les choix visibles dans le code :
811
845
 
812
846
  - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
813
- socket (`wsHeartbeat.ts:69`).
847
+ socket (`wsHeartbeat.ts:137`).
814
848
  - **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
815
- `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`).
816
850
  - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
817
851
  DI et l'ALS : un flood coûte une recherche dans une table de hachage.
818
852
  - **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
819
- (`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.
820
854
  - **Fichiers statiques en repli** — depuis la bascule « router d'abord », une requête qui matche une
821
855
  route ne paie plus l'appel disque de `serve-static` (**+28 % de requêtes par seconde** mesurés en
822
856
  production mono-processus).
@@ -837,19 +871,19 @@ demande le backplane realtime.
837
871
 
838
872
  | Domaine | Norme | Ancrage |
839
873
  | ------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
840
- | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1009`) |
874
+ | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:1093`) |
841
875
  | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:187`) |
842
876
  | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
843
877
  | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
844
878
  | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
845
- | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:113`) |
846
- | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`) |
847
- | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`) |
848
- | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`) |
849
- | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:621`) |
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`) |
850
884
  | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
851
- | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:263`) |
852
- | Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:446`) |
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`) |
853
887
 
854
888
  ## ⚠️ Pièges (symptôme → cause → correction)
855
889
 
@@ -857,13 +891,13 @@ demande le backplane realtime.
857
891
  | --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
858
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 |
859
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é |
860
- | `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`) |
861
895
  | Le WSS ignore `websocket.maxPayload` | Le serveur secure lit `websocketSecure`, section distincte | Régler **les deux** sections (`server-websocket-secure.ts:50`) |
862
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 |
863
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`) |
864
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 |
865
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 |
866
- | 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`) |
867
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 |
868
902
  | Avertissement navigateur en HTTPS de développement | Certificat auto-signé (mkcert absent) | `brew install mkcert nss && mkcert -install`, puis redémarrer |
869
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`) |
@@ -889,7 +923,7 @@ l'origine du transport.
889
923
 
890
924
  `proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
891
925
  confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
892
- résumé de certificat vient de `Certificate.describe()` (`certificates.ts:758`), source unique partagée
926
+ résumé de certificat vient de `Certificate.describe()` (`certificates.ts:973`), source unique partagée
893
927
  par la commande, le boot et un futur écran d'administration.
894
928
 
895
929
  **Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils