@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +4 -2
  3. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  4. package/dist/nodefony/command/certificatesCommand.js +1 -1
  5. package/dist/nodefony/command/networkCommand.js +1 -1
  6. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  7. package/dist/nodefony/config/config.js +22 -1
  8. package/dist/nodefony/interfaces/IRouting.js +1 -0
  9. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  10. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  11. package/dist/nodefony/service/audit-logger.js +16 -13
  12. package/dist/nodefony/service/certificates.js +25 -13
  13. package/dist/nodefony/service/error-renderer.js +11 -8
  14. package/dist/nodefony/service/http-kernel.js +340 -163
  15. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  16. package/dist/nodefony/service/servers/clientError.js +2 -2
  17. package/dist/nodefony/service/servers/server-http.js +20 -20
  18. package/dist/nodefony/service/servers/server-https.js +38 -39
  19. package/dist/nodefony/service/servers/server-static.js +13 -17
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
  21. package/dist/nodefony/service/servers/server-websocket.js +8 -7
  22. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  23. package/dist/nodefony/service/sessions/sessions-service.js +19 -12
  24. package/dist/nodefony/service/trace.js +1 -1
  25. package/dist/nodefony/service/upload/upload-service.js +8 -7
  26. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  27. package/dist/nodefony/src/context/Context.js +34 -19
  28. package/dist/nodefony/src/context/forwarded.js +4 -4
  29. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  30. package/dist/nodefony/src/context/http/Request.js +93 -44
  31. package/dist/nodefony/src/context/http/Response.js +107 -51
  32. package/dist/nodefony/src/context/http/parser.js +9 -10
  33. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  34. package/dist/nodefony/src/context/http2/Response.js +53 -25
  35. package/dist/nodefony/src/context/metaData.js +1 -1
  36. package/dist/nodefony/src/context/trustProxy.js +4 -3
  37. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  38. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  39. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  40. package/dist/nodefony/src/cookies/cookie.js +3 -3
  41. package/dist/nodefony/src/errors/httpError.js +6 -6
  42. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  43. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  44. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  45. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  46. package/dist/nodefony/src/servers/portBinder.js +8 -21
  47. package/dist/nodefony/src/session/session.js +18 -10
  48. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  49. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/nodefony/config/config.d.ts +22 -0
  52. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  53. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  54. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  55. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  56. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  57. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  58. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  59. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  60. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  61. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  62. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  63. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  64. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  65. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  66. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  67. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  68. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  69. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  71. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  72. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  73. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  74. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  76. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  77. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  78. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  79. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  80. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  81. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  82. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  83. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  84. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  85. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  86. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  87. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  88. package/docs/cookies.md +20 -20
  89. package/docs/observabilite.md +25 -25
  90. package/docs/rate-limit.md +18 -18
  91. package/docs/servers.md +52 -52
  92. package/docs/session.md +65 -60
  93. package/docs/upload.md +36 -30
  94. package/package.json +7 -7
package/docs/servers.md CHANGED
@@ -122,13 +122,13 @@ 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:174`) : les clients modernes négocient `h2` par ALPN, les autres restent en HTTP/1.1
125
+ `server-https.ts:187`) : 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
127
  (`server-https.ts:222`).
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:1549`) —, hérite de la même
131
+ _est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1619`) —, 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]
@@ -249,7 +249,7 @@ 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:1255`), puis affiche les URL réellement
252
+ Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1274`), puis affiche les URL réellement
253
253
  en écoute — le récap de développement liste HTTP, HTTP/2, WS et WSS dans cet ordre
254
254
  (`BootReporter.ts:389`) :
255
255
 
@@ -264,7 +264,7 @@ en écoute — le récap de développement liste HTTP, HTTP/2, WS et WSS dans ce
264
264
  ```
265
265
 
266
266
  Hors écran animé (production, CI, `--debug`), ce sont les bannières par serveur qui sortent
267
- (`ServerHttp.showBanner()`, `server-http.ts:232`, appelées par le kernel — `Kernel.ts:561`) :
267
+ (`ServerHttp.showBanner()`, `server-http.ts:242`, appelées par le kernel — `Kernel.ts:561`) :
268
268
 
269
269
  ```text
270
270
  Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
@@ -327,34 +327,34 @@ Choisir en cinq secondes :
327
327
  | `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
328
328
  | `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
329
329
 
330
- L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1046`) : chaque serveur est
331
- consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:235`).
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`).
332
332
  Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
333
333
 
334
334
  ### `server-http` — HTTP/1.1 en clair
335
335
 
336
336
  Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.createServer()`
337
- (`server-http.ts:74`) crée le `http.Server`, applique les réglages de transport, branche
337
+ (`server-http.ts:83`) crée le `http.Server`, applique les réglages de transport, branche
338
338
  `onHttpRequest`, puis écoute selon la politique de port.
339
339
 
340
340
  - **Actif** si `servers.http` n'est pas `false` — la décision est prise dès le constructeur
341
341
  (`server-http.ts:56`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
342
- `server-http.ts:67`).
342
+ `server-http.ts:74`).
343
343
  - **Réglages appliqués** : `requestTimeout` (anti slow-loris), `maxHeadersCount`, `timeout` de socket
344
344
  (qui émet un événement `onTimeout`), `keepAliveTimeout`.
345
345
  - **Erreurs de protocole** : l'événement `clientError` est traité explicitement
346
- (`server-http.ts:167`) — voir la section Résilience, c'est un piège Node à part entière.
346
+ (`server-http.ts:186`) — voir la section Résilience, c'est un piège Node à part entière.
347
347
 
348
348
  ### `server-https` — TLS, et HTTP/2 par défaut
349
349
 
350
- Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:39`) :
350
+ Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:100`) :
351
351
 
352
352
  - **`"2.0"` (défaut)** → `http2.createSecureServer` avec `allowHTTP1: true`
353
- (`ServerHttps.createServerH2()`, `server-https.ts:174`). Les bornes anti-DoS HTTP/2 ne sont posées
353
+ (`ServerHttps.createServerH2()`, `server-https.ts:187`). Les bornes anti-DoS HTTP/2 ne sont posées
354
354
  **que si elles sont configurées**, pour ne pas écraser les défauts de Node
355
- (`maxSessionMemory`, `server-https.ts:185`).
355
+ (`maxSessionMemory`, `server-https.ts:215`).
356
356
  Les erreurs de session et de flux sont journalisées sans tuer le serveur
357
- (`sessionError`, `server-https.ts:274`).
357
+ (`sessionError`, `server-https.ts:296`).
358
358
  - **`"1.1"`** → `https.createServer` classique.
359
359
 
360
360
  Le certificat vient du service `certificates`, lu au moment de la création via
@@ -375,7 +375,7 @@ automatiquement un éventuel décalage de port.
375
375
  réglages **forcés** par Nodefony : `server` et `clientTracking: true`, requis par `broadcast()` et par
376
376
  le battement de cœur (`server-websocket.ts:80`).
377
377
  - **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts:87`) et par connexion
378
- (`trackPong()`, `server-websocket.ts:107`).
378
+ (`trackPong()`, `server-websocket.ts:103`).
379
379
  - **Arrêt** : il s'inscrit en tête des écouteurs de terminaison
380
380
  (`prependOnceListener`, `server-websocket.ts:91`) — l'ordre
381
381
  compte, voir la section Arrêt gracieux.
@@ -424,16 +424,16 @@ Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaul
424
424
  Table dérivée de `httpServerSchema` (`config.ts:272`) ; la section `https` reprend les mêmes clés et en
425
425
  ajoute une (`httpsServerSchema`, `config.ts:336`).
426
426
 
427
- | Option | Type | Défaut | Effet |
428
- | ---------------------------- | ----- | -------- | -------------------------------------------------------------------------------- |
429
- | `maxHeadersCount` | int | `2000` | Nombre maximum d'en-têtes par requête — anti _header flooding_. |
430
- | `keepAliveTimeout` | ms | `5000` | Délai de réutilisation de la socket TCP entre deux requêtes. |
431
- | `timeout` | ms | `120000` | Timeout global de socket. `0` = désactivé. |
432
- | `requestTimeout` | ms | `30000` | Délai de réception de la requête complète — **anti slow-loris**. |
433
- | `responseTimeout` | ms | `30000` | Délai d'envoi de la réponse complète (couche pipeline). |
434
- | `shutdownTimeout` | ms | `5000` | Drain au shutdown avant destruction forcée. Garder < grâce orchestrateur. |
435
- | `headers` | objet | `null` | En-têtes ajoutés à toutes les réponses. |
436
- | `rejectUnauthorized` (https) | bool | `false` | Rejette les certificats invalides. `false` en dev (auto-signés), `true` en prod. |
427
+ | Option | Type | Défaut | Effet |
428
+ | ---------------------------- | ----- | ------- | -------------------------------------------------------------------------------- |
429
+ | `maxHeadersCount` | int | `2000` | Nombre maximum d'en-têtes par requête — anti _header flooding_. |
430
+ | `keepAliveTimeout` | ms | `5000` | Délai de réutilisation de la socket TCP entre deux requêtes. |
431
+ | `timeout` | ms | `30000` | Timeout global de socket, aligné sur `responseTimeout`. `0` = désactivé. |
432
+ | `requestTimeout` | ms | `30000` | Délai de réception de la requête complète — **anti slow-loris**. |
433
+ | `responseTimeout` | ms | `30000` | Délai d'envoi de la réponse complète (couche pipeline). |
434
+ | `shutdownTimeout` | ms | `5000` | Drain au shutdown avant destruction forcée. Garder < grâce orchestrateur. |
435
+ | `headers` | objet | `null` | En-têtes ajoutés à toutes les réponses. |
436
+ | `rejectUnauthorized` (https) | bool | `false` | Rejette les certificats invalides. `false` en dev (auto-signés), `true` en prod. |
437
437
 
438
438
  Ces deux sections sont **permissives** (`z.looseObject`) : toute option supplémentaire de
439
439
  `http.Server` / `net.Server` / TLS (`insecureHTTPParser`, `ciphers`, `minVersion`…) est transmise telle
@@ -442,7 +442,7 @@ quelle à Node. C'est délibéré — un schéma strict effacerait silencieuseme
442
442
  ### Niveau 2 — HTTP/2
443
443
 
444
444
  Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
445
- (`maxSessionMemory`, `server-https.ts:185`).
445
+ (`maxSessionMemory`, `server-https.ts:215`).
446
446
 
447
447
  | Option | Type | Défaut | Effet |
448
448
  | ---------------------- | ---- | ------ | ----------------------------------------------------------------------------- |
@@ -484,7 +484,7 @@ panne, c'est une nuisance.
484
484
 
485
485
  ### La règle
486
486
 
487
- `resolvePortPolicy()` (`portBinder.ts:93`) tranche selon l'environnement, et la valeur explicite gagne
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
488
488
  toujours :
489
489
 
490
490
  | Environnement | Défaut | Pourquoi |
@@ -507,17 +507,17 @@ détails d'implémentation valent d'être connus, parce qu'ils expliquent des co
507
507
  1. **On retente au `listen()`, jamais après une sonde.** Demander « le port est-il libre ? » puis
508
508
  binder est une course : entre la réponse et le bind, un autre processus peut prendre le port. Le
509
509
  `listen()` est atomique — on retente donc sur l'échec réel (`bindWithFallback()`,
510
- `portBinder.ts:281`).
510
+ `portBinder.ts:263`).
511
511
  2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
512
512
  ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
513
- sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:123`, réservation `portBinder.ts:123`).
513
+ sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:105`, réservation `portBinder.ts:105`).
514
514
  3. **Le gestionnaire d'erreur durable est posé APRÈS le bind.** Attaché avant, il verrait passer les
515
515
  `EADDRINUSE` de repli et terminerait le kernel en croyant à une panne
516
- (`ServerHttp.attachErrorHandler()`, `server-http.ts:186`).
516
+ (`ServerHttp.attachErrorHandler()`, `server-http.ts:199`).
517
517
 
518
518
  Quand le bind échoue pour de bon — `strict`, ou tous les replis épuisés, ou une erreur qui n'est pas un
519
519
  conflit de port — c'est **fatal** : message explicite puis terminaison du processus
520
- (`ServerHttp.reportBindError()`, `server-http.ts:199`). Un serveur qui n'écoute pas ne doit jamais
520
+ (`ServerHttp.reportBindError()`, `server-http.ts:212`). Un serveur qui n'écoute pas ne doit jamais
521
521
  laisser un processus se croire démarré.
522
522
 
523
523
  ### Le corollaire : les ports effectifs sont publiés
@@ -577,8 +577,8 @@ export default defineConfig(() => ({
577
577
  ### Ce que le chemin `explicit` évite
578
578
 
579
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:227`) : en production avec un certificat
581
- fourni, elle n'entre jamais dans le processus (`certificates.ts:227`).
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`).
582
582
 
583
583
  ### Conformité de l'auto-signé
584
584
 
@@ -588,7 +588,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
588
588
  | Exigence | Norme | Mise en œuvre |
589
589
  | --------------------------------------- | -------------------- | --------------------------------------------------------- |
590
590
  | 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:292`) |
591
+ | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:302`) |
592
592
  | Le SAN fait foi, pas le CN | RFC 6125 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
593
593
  | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
594
594
  | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
@@ -596,7 +596,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
596
596
  ### Régénération automatique
597
597
 
598
598
  Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
599
- (`certificates.ts:570`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
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
600
600
  plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
601
601
  contrôle laisserait un certificat obsolète en place indéfiniment.
602
602
 
@@ -618,7 +618,7 @@ sont ignorés** (`config.ts:965`), et l'IP retenue est celle de la socket réell
618
618
  | `"loopback"`, `"linklocal"`, `"uniquelocal"` | Préréglages de plages privées. |
619
619
 
620
620
  La politique est compilée **une seule fois** au premier usage (`HttpKernel.getTrustProxyChecker()`,
621
- `http-kernel.ts:538`, via `buildTrustProxy()`, `trustProxy.ts:100`) : aucune structure allouée par
621
+ `http-kernel.ts:562`, via `buildTrustProxy()`, `trustProxy.ts:100`) : aucune structure allouée par
622
622
  requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauche** depuis la socket réelle
623
623
  (`resolveForwarded()`, `forwarded.ts:253`) — conforme RFC 7239 et à la recommandation OWASP.
624
624
 
@@ -626,7 +626,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
626
626
 
627
627
  Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
628
628
  kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
629
- `http-kernel.ts:941`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
629
+ `http-kernel.ts:964`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
630
630
  d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
631
631
  filtre déjà le `Host` (`config.ts:974`).
632
632
 
@@ -635,13 +635,13 @@ filtre déjà le `Host` (`config.ts:974`).
635
635
  Les navigateurs **n'appliquent pas CORS aux WebSockets**. Sans contrôle, une page tierce peut ouvrir un
636
636
  WS authentifié par le cookie de session de la victime (CSWSH, OWASP WSTG-CLNT-10). Nodefony exige donc
637
637
  par défaut que l'`Origin` du handshake corresponde au `Host` servi
638
- (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:599`), avec tolérance loopback en développement
638
+ (`HttpKernel.checkWebsocketOrigin()`, `http-kernel.ts:621`), avec tolérance loopback en développement
639
639
  et allowlist explicite pour les SPA cross-origine. Un refus se solde par une fermeture **1008 (Policy
640
640
  Violation)**, jamais par un code HTTP.
641
641
 
642
642
  > [!NOTE]
643
643
  > Une requête **sans** `Origin` (client non navigateur : script, agent, test) est acceptée
644
- > (`http-kernel.ts:551`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
644
+ > (`http-kernel.ts:630`). Ce n'est pas un trou : un attaquant non navigateur n'a aucun besoin de CSWSH,
645
645
  > il se connecte directement. Le contrôle protège les **utilisateurs**, pas le port.
646
646
 
647
647
  ## Probes de santé — `/livez` et `/readyz`
@@ -658,8 +658,8 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
658
658
  protéger.
659
659
 
660
660
  Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
661
- (`http-kernel.ts:949`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
662
- réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:473`). Et surtout : **avant le
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
663
663
  rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
664
664
 
665
665
  | Option | Type | Défaut | Effet |
@@ -732,8 +732,8 @@ inséré **en tête** et donc s'exécuter **en premier** (`http-kernel.ts:515`).
732
732
  load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette information le temps de se
733
733
  propager.
734
734
 
735
- **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:117`)
736
- envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:117`).
735
+ **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:113`)
736
+ envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:113`).
737
737
  Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
738
738
  indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
739
739
 
@@ -745,7 +745,7 @@ réponses en cours, sockets inactives fermées, destruction forcée au-delà de
745
745
  > [!IMPORTANT]
746
746
  > L'ordre WS-avant-HTTP n'est pas cosmétique. Les serveurs WebSocket s'inscrivent en
747
747
  > `prependOnceListener` (`server-websocket.ts:91`), les serveurs HTTP en `once`
748
- > (`server-http.ts:151`). Inversé, le terminator détruirait les sockets déjà upgradées **sans** frame
748
+ > (`server-http.ts:170`). Inversé, le terminator détruirait les sockets déjà upgradées **sans** frame
749
749
  > Close : retour du 1006 pour tous les clients temps réel.
750
750
 
751
751
  Deux garde-fous encadrent la séquence : `shutdownTimeout` **par serveur** (le drain nominal) et
@@ -784,9 +784,9 @@ processus à l'arrêt.
784
784
 
785
785
  L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
786
786
  IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
787
- (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1549`). Le `101` étant déjà émis par `ws`, un `429`
787
+ (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1619`). Le `101` étant déjà émis par `ws`, un `429`
788
788
  est impossible → la connexion est fermée en **1013 « Try Again Later »**
789
- (`rateLimiter`, `http-kernel.ts:292`), sans
789
+ (`rateLimiter`, `http-kernel.ts:1644`), sans
790
790
  journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
791
791
 
792
792
  Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
@@ -809,7 +809,7 @@ par seconde. Les choix visibles dans le code :
809
809
  - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
810
810
  socket (`wsHeartbeat.ts:69`).
811
811
  - **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
812
- `trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:248`, `http-kernel.ts:248`).
812
+ `trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:407`, `http-kernel.ts:407`).
813
813
  - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
814
814
  DI et l'ALS : un flood coûte une recherche dans une table de hachage.
815
815
  - **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
@@ -834,18 +834,18 @@ demande le backplane realtime.
834
834
 
835
835
  | Domaine | Norme | Ancrage |
836
836
  | ------------------------------------- | ------------------ | -------------------------------------------------------------------------- |
837
- | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:949`) |
838
- | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:174`) |
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
839
  | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
840
840
  | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
841
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:117`) |
842
+ | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:113`) |
843
843
  | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`) |
844
844
  | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`) |
845
845
  | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`) |
846
- | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:599`) |
846
+ | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:621`) |
847
847
  | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
848
- | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:292`) |
848
+ | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:302`) |
849
849
  | Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:442`) |
850
850
 
851
851
  ## ⚠️ Pièges (symptôme → cause → correction)
@@ -886,7 +886,7 @@ l'origine du transport.
886
886
 
887
887
  `proxy:generate` mérite un mot : la configuration nginx/HAProxy est **dérivée** des domaines de
888
888
  confiance, des ports effectifs et des dossiers statiques montés — donc elle ne diverge pas du code. Le
889
- résumé de certificat vient de `Certificate.describe()` (`certificates.ts:853`), source unique partagée
889
+ résumé de certificat vient de `Certificate.describe()` (`certificates.ts:862`), source unique partagée
890
890
  par la commande, le boot et un futur écran d'administration.
891
891
 
892
892
  **Runtime.** `nodefony status` et `nodefony stop` lisent les ports effectifs publiés au boot ; ils
package/docs/session.md CHANGED
@@ -69,8 +69,8 @@ flowchart TD
69
69
  ```
70
70
 
71
71
  Le point d'activation est **unique et commun aux deux transports** : `HttpKernel.startSession()`
72
- (`http-kernel.ts:1152`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
73
- (`http-kernel.ts:1137`) — sans intent de route ni cookie entrant, **aucune session n'est ouverte**.
72
+ (`http-kernel.ts:1168`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
73
+ (`http-kernel.ts:1207`) — sans intent de route ni cookie entrant, **aucune session n'est ouverte**.
74
74
 
75
75
  ## 📖 Lexique
76
76
 
@@ -122,7 +122,7 @@ défaut** dans Nodefony :
122
122
  l'identifiant (`AuthFlow` — voir plus bas).
123
123
  - **Exploitation prolongée d'un identifiant volé.** Une session maintenue artificiellement vivante
124
124
  resterait exploitable indéfiniment. → l'**absolute timeout** borne l'âge depuis la création et n'est
125
- **jamais** prolongé (`Session.isValidSession()`, `session.ts:366`), en plus de l'idle timeout.
125
+ **jamais** prolongé (`Session.isValidSession()`, `session.ts:371`), en plus de l'idle timeout.
126
126
 
127
127
  > [!IMPORTANT]
128
128
  > Le cookie **ne chiffre rien** et n'a pas à le faire : il ne porte qu'un numéro. La sécurité repose
@@ -135,16 +135,16 @@ Quatre partis pris, chacun vérifiable dans le code.
135
135
 
136
136
  **1. Le cookie ne transporte que l'identifiant.** `Session.getSession()` lit la **valeur brute** du
137
137
  cookie, sans déchiffrement (`session.ts:160`) ; l'identifiant vient de `Session.generateId()`
138
- (`session.ts:226`). Modèle BFF : le web reste sur un cookie opaque, le JWT est réservé aux API et aux
138
+ (`session.ts:231`). Modèle BFF : le web reste sur un cookie opaque, le JWT est réservé aux API et aux
139
139
  agents (voir [Firewall](../../security/docs/firewall.md)).
140
140
 
141
141
  **2. La session est paresseuse.** Elle n'existe que si une route la demande — `@UseSession`, ou la
142
142
  seule présence d'un paramètre `@Session` — ou si un cookie arrive déjà : c'est la garde de
143
- `HttpKernel.startSession()` (`http-kernel.ts:1152`). Une route publique ne paie **ni lecture de store,
143
+ `HttpKernel.startSession()` (`http-kernel.ts:1168`). Une route publique ne paie **ni lecture de store,
144
144
  ni `Set-Cookie`**.
145
145
 
146
146
  **3. Un seul modèle d'état pour le web et le temps réel.** Le même `startSession()` sert
147
- `HttpKernel.onRequestEnd()` (`http-kernel.ts:1443`) et `HttpKernel.onConnect()` (`http-kernel.ts:1711`) ;
147
+ `HttpKernel.onRequestEnd()` (`http-kernel.ts:1491`) et `HttpKernel.onConnect()` (`http-kernel.ts:1779`) ;
148
148
  l'activité HTTP **ou** WS prolonge la même session (`Session.touchIfNeeded()`, `session.ts:421`).
149
149
 
150
150
  **4. L'administration ne voit jamais un identifiant.** Un opérateur manipule une `ref`, HMAC tronqué
@@ -281,24 +281,24 @@ faute de `Secure` (`Context.getSessionCookieName()`, `Context.ts:714`).
281
281
  Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:782`) et son sous-schéma
282
282
  `sessionCookieSchema` (`config.ts:748`).
283
283
 
284
- | Option | Type | Défaut | Effet |
285
- | ------------------- | ------- | ------------ | --------------------------------------------------------------------------------- |
286
- | `store` | string | `"auto"` | Backend de persistance — voir la résolution ci-dessous (`config.ts:795`). |
287
- | `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts:770`). |
288
- | `strictMode` | bool | `true` | Un identifiant inconnu du store est rejeté → session neuve (anti-fixation). |
289
- | `idleTimeoutS` | int ≥ 0 | `1800` | Inactivité max (30 min). `0` = pas d'expiration par inactivité (`config.ts:823`). |
290
- | `absoluteTimeoutS` | int ≥ 0 | `43200` | Âge max depuis la création (12 h), **jamais** prolongé. `0` = désactivé. |
291
- | `gcIntervalS` | int ≥ 0 | `600` | Période de purge des sessions expirées, hors requête. `0` = timer désarmé. |
292
- | `gcJitter` | bool | `true` | Décale le départ du GC par process (anti _thundering herd_ sur un store partagé). |
293
- | `refererCheck` | bool | `false` | Lie la session à l'hôte de création (défense en profondeur, `session.ts:366`). |
294
- | `cookie.maxAge` | int ≥ 0 | `0` | `0` = cookie de session (effacé à la fermeture du navigateur). |
295
- | `cookie.httpOnly` | bool | `true` | Inaccessible depuis JavaScript — anti-XSS. |
296
- | `cookie.secure` | bool | `true` | Envoyé sur TLS uniquement. |
297
- | `cookie.signed` | bool | `false` | Signe le cookie avec le secret HMAC du kernel. |
298
- | `cookie.hostPrefix` | enum | `"auto"` | `__Host-` : `auto` (sur TLS) \| `true` (toujours) \| `false` (jamais). |
284
+ | Option | Type | Défaut | Effet |
285
+ | ------------------- | ------- | ------------ | ---------------------------------------------------------------------------------------- |
286
+ | `store` | string | `"auto"` | Backend de persistance — voir la résolution ci-dessous (`config.ts:795`). |
287
+ | `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts:770`). |
288
+ | `strictMode` | bool | `true` | Un identifiant inconnu du store est rejeté → session neuve (anti-fixation). |
289
+ | `idleTimeoutS` | int ≥ 0 | `1800` | Inactivité max (30 min). `0` = pas d'expiration par inactivité (`config.ts:823`). |
290
+ | `absoluteTimeoutS` | int ≥ 0 | `43200` | Âge max depuis la création (12 h), **jamais** prolongé. `0` = désactivé. |
291
+ | `gcIntervalS` | int ≥ 0 | `600` | Période de purge des sessions expirées, hors requête. `0` = timer désarmé. |
292
+ | `gcJitter` | bool | `true` | Décale le départ du GC par process (anti _thundering herd_ sur un store partagé). |
293
+ | `refererCheck` | bool | `false` | Lie la session à l'hôte de création — ⚠️ court-circuite les expirations (voir plus bas). |
294
+ | `cookie.maxAge` | int ≥ 0 | `0` | `0` = cookie de session (effacé à la fermeture du navigateur). |
295
+ | `cookie.httpOnly` | bool | `true` | Inaccessible depuis JavaScript — anti-XSS. |
296
+ | `cookie.secure` | bool | `true` | Envoyé sur TLS uniquement. |
297
+ | `cookie.signed` | bool | `false` | Signe le cookie avec le secret HMAC du kernel. |
298
+ | `cookie.hostPrefix` | enum | `"auto"` | `__Host-` : `auto` (sur TLS) \| `true` (toujours) \| `false` (jamais). |
299
299
 
300
300
  `SameSite` n'est pas dans ce bloc : il vient des options de cookie génériques, dont le défaut est
301
- `Lax` (`defaultCookieOptions`, `cookie.ts:48`).
301
+ `Lax` (`cookieDefaultSettings`, `cookie.ts:39`).
302
302
 
303
303
  > [!WARNING]
304
304
  > `idleTimeoutS: 0` **et** `absoluteTimeoutS: 0` désactivent les deux bornes : une session ne meurt
@@ -310,7 +310,7 @@ Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:782`) et
310
310
 
311
311
  `auto` n'est pas un store : c'est une sentinelle résolue une fois, au boot, par `resolveAutoStore()`
312
312
  (`config/infra.ts:297`), puis journalisée. Elle suit **l'infra que tu as déclarée**, bornée aux stores
313
- réellement enregistrés (`SessionsService.initializeStorage()`, `sessions-service.ts:241`).
313
+ réellement enregistrés (`SessionsService.initializeStorage()`, `sessions-service.ts:258`).
314
314
 
315
315
  ```mermaid
316
316
  flowchart TD
@@ -351,7 +351,7 @@ Ce qui change, c'est la topologie et la façon d'expirer.
351
351
 
352
352
  ### `memory` — l'implémentation de référence
353
353
 
354
- Store built-in de `@nodefony/http`, enregistré d'office (`sessions-service.ts:860`). Les sessions vivent
354
+ Store built-in de `@nodefony/http`, enregistré d'office (`sessions-service.ts:913`). Les sessions vivent
355
355
  dans une `Map` du process : elles **disparaissent au redémarrage** et ne sont **pas partagées** entre
356
356
  pods — c'est un choix (mesurer le framework sans le goulot disque/SQL), pas une limite.
357
357
 
@@ -385,7 +385,7 @@ Ici l'expiration **idle** est portée par Redis lui-même : `SET … EX` pose le
385
385
  (`@nodefony/redis/nodefony/src/SessionStorage.ts:168`) — aucun balayage périodique.
386
386
 
387
387
  L'absolute timeout, lui, n'est pas exprimable par un TTL glissant : il reste honoré **à la lecture**
388
- par `Session.isValidSession()` (`session.ts:366`). Une entrée trop vieille peut donc survivre côté
388
+ par `Session.isValidSession()` (`session.ts:371`). Une entrée trop vieille peut donc survivre côté
389
389
  Redis jusqu'à son TTL idle, mais elle est **refusée à la reprise**.
390
390
 
391
391
  Capacités réduites, annoncées et non simulées : la pagination est **par curseur** (pas de `total`, pas
@@ -439,12 +439,17 @@ sequenceDiagram
439
439
  → `resume()` (`session.ts:177`), sinon `create()` (`session.ts:204`) qui tire un identifiant CSPRNG,
440
440
  pose le cookie et marque la session à persister.
441
441
 
442
- **Validation à la reprise.** `Session.isValidSession()` (`session.ts:365`) applique dans l'ordre le
443
- `refererCheck` (si activé), l'**absolute** (âge depuis `created`, `session.ts:381`), puis l'**idle**
444
- (depuis `updated`, `session.ts:394`). Échec → `invalidate()` (`session.ts:284`) détruit l'entrée et
445
- recrée une session vierge.
442
+ **Validation à la reprise.** `Session.isValidSession()` (`session.ts:371`) applique l'**absolute**
443
+ (âge depuis `created`) puis l'**idle** (depuis `updated`). Échec → `invalidate()` (`session.ts:290`)
444
+ détruit l'entrée et recrée une session vierge.
446
445
 
447
- **Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts:429`) n'écrit **que** si la
446
+ > [!WARNING]
447
+ > Avec `refererCheck: true`, `isValidSession()` rend son verdict sur la **seule** correspondance
448
+ > d'hôte (`Session.checkSecureReferer()`) et retourne aussitôt : les bornes **absolute** et
449
+ > **idle** ne sont alors **pas** évaluées. Tant que ce comportement n'est pas corrigé, ne pas
450
+ > activer `refererCheck` quand l'expiration des sessions compte — c'est-à-dire presque toujours.
451
+
452
+ **Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts:463`) n'écrit **que** si la
448
453
  session est `dirty` et non `readOnly` ; sinon il appelle `Session.touchIfNeeded()` (`session.ts:421`),
449
454
  qui prolonge l'idle **sans réécrire le blob**, et seulement au-delà d'une demi-vie d'idle
450
455
  (`session.ts:445`). Une requête de lecture coûte donc au pire un `UPDATE` d'horodatage toutes les
@@ -452,14 +457,14 @@ qui prolonge l'idle **sans réécrire le blob**, et seulement au-delà d'une dem
452
457
 
453
458
  **Anti-résurrection**, sur deux niveaux. `Session.destroy()` remet `mutated = false` (`session.ts:307`)
454
459
  pour que la sauvegarde de fin de requête ne réécrive pas ce qu'on vient de supprimer. Surtout, **tout**
455
- store est décoré par `RevocationGuardStorage` (`sessions-service.ts:279`) : `destroy()` pose une
456
- **pierre tombale** de 5 minutes (`RevocationGuardStorage.ts:144`) qui refuse ensuite tout `write`
457
- (`RevocationGuardStorage.ts:151`) **et tout `touch`** (`RevocationGuardStorage.ts:151`) du même
460
+ store est décoré par `RevocationGuardStorage` (`sessions-service.ts:309`) : `destroy()` pose une
461
+ **pierre tombale** de 5 minutes (`RevocationGuardStorage.ts:156`) qui refuse ensuite tout `write`
462
+ (`RevocationGuardStorage.ts:163`) **et tout `touch`** (`RevocationGuardStorage.ts:130`) du même
458
463
  identifiant — ce qui couvre la requête « en vol » d'un autre client.
459
464
 
460
465
  **Purge hors requête.** Un `GcScheduler` est armé au `onReady`, désarmé au `onTerminate`
461
466
  (`sessions-service.ts:284-313`). La passe métier nue, `SessionsService.runGc()`
462
- (`sessions-service.ts:470`), est publique exprès : un CronJob Kubernetes peut l'appeler à la place du
467
+ (`sessions-service.ts:496`), est publique exprès : un CronJob Kubernetes peut l'appeler à la place du
463
468
  timer (`gcIntervalS: 0`).
464
469
 
465
470
  ## Entités de persistance
@@ -498,17 +503,17 @@ C'est le différenciateur du framework appliqué à l'état de session : un seul
498
503
  <!-- prettier-ignore -->
499
504
  | Aspect | HTTP | WebSocket |
500
505
  | --- | --- | --- |
501
- | Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:1443`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts:1711`) |
506
+ | Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:1491`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts:1779`) |
502
507
  | Lecture du cookie | constructeur du contexte | constructeur, même nom effectif (`WebsocketContext.ts:172`) |
503
508
  | Sauvegarde | fin de requête | après **chaque frame** traitée (`WebsocketContext.ts:302`) |
504
- | Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:1518`) |
509
+ | Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:1566`) |
505
510
  | Portée ALS | une requête | **handshake + toutes les frames** (`http-kernel.ts:1495`) |
506
511
 
507
512
  La conséquence pratique la plus utile : côté WebSocket, la bulle `AsyncLocalStorage` ouverte au
508
- handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:436`). L'identité résolue une fois est donc
513
+ handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:455`). L'identité résolue une fois est donc
509
514
  disponible à chaque frame sans relire la base — c'est ce dont profite
510
515
  `FirewallRealtimeAuthenticator.supports()` (`FirewallRealtimeAuthenticator.ts:80`), câblé automatiquement
511
- par le firewall sur les zones temps réel protégées (`firewall.ts:300`).
516
+ par le firewall sur les zones temps réel protégées (`firewall.ts:297`).
512
517
 
513
518
  > [!WARNING]
514
519
  > Rien à écrire dans `initialize()` : il n'existe **pas** de `Controller.startSession()`. La session WS
@@ -535,20 +540,20 @@ affiche.
535
540
 
536
541
  | Surface | Méthode | Portée |
537
542
  | ------------------------ | ----------------------------------------------- | ------------------------------------------- |
538
- | Déconnexion locale | `Session.destroy()` (`session.ts:300`) | la session courante + pierre tombale |
539
- | Révocation par un admin | `destroyByRef()` (`sessions-service.ts:707`) | une session désignée par sa `ref` publique |
540
- | « Déconnecter partout » | `destroyByUser()` (`sessions-service.ts:737`) | toutes les sessions d'un utilisateur |
541
- | « Mes appareils » (self) | `destroyOwnByRef()` (`sessions-service.ts:834`) | une session, **restreinte au propriétaire** |
543
+ | Déconnexion locale | `Session.destroy()` (`session.ts:306`) | la session courante + pierre tombale |
544
+ | Révocation par un admin | `destroyByRef()` (`sessions-service.ts:745`) | une session désignée par sa `ref` publique |
545
+ | « Déconnecter partout » | `destroyByUser()` (`sessions-service.ts:775`) | toutes les sessions d'un utilisateur |
546
+ | « Mes appareils » (self) | `destroyOwnByRef()` (`sessions-service.ts:872`) | une session, **restreinte au propriétaire** |
542
547
 
543
548
  Deux finesses valent d'être connues.
544
549
 
545
550
  `destroyByUser()` ne fait pas un seul passage : il **repasse jusqu'à ce qu'un passage complet ne
546
- détruise plus rien** (`sessions-service.ts:737`), car supprimer en parcourant décale les rangs sous un
551
+ détruise plus rien** (`sessions-service.ts:775`), car supprimer en parcourant décale les rangs sous un
547
552
  curseur offset. Une révocation « partout » qui en laisserait une n'est pas une imprécision, c'est une
548
553
  faille — on rend donc la main avec la preuve, pas l'espoir.
549
554
 
550
555
  `destroyOwnByRef()` ferme l'IDOR **par construction** : parcours restreint aux sessions du demandeur,
551
- et appartenance **re-vérifiée** avant même de comparer la `ref` (`sessions-service.ts:840`). Une
556
+ et appartenance **re-vérifiée** avant même de comparer la `ref` (`sessions-service.ts:863`). Une
552
557
  `ref` d'autrui est structurellement introuvable.
553
558
 
554
559
  ### Redaction — l'identifiant ne sort jamais du process
@@ -561,7 +566,7 @@ Trois barrières superposées :
561
566
  **liste blanche** : `ref`, `user`, `authenticated`, `ip`, `ua`, dates. Jamais un `delete` après coup.
562
567
  3. La `ref` elle-même est un HMAC tronqué non réversible (`computeSessionRef()`,
563
568
  `sessions-service.ts:100`) ; la clé est dérivée du certificat au boot et n'est jamais sérialisée
564
- (`SessionsService.sessionRef()`, `sessions-service.ts:528`).
569
+ (`SessionsService.sessionRef()`, `sessions-service.ts:564`).
565
570
 
566
571
  ### Récapitulatif des défenses actives par défaut
567
572
 
@@ -569,20 +574,20 @@ Trois barrières superposées :
569
574
  | --------------------------------- | ------------------------------------------------- | -------------------------------------------------- |
570
575
  | Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts:748`) |
571
576
  | Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts:714`) |
572
- | Requête inter-sites | `SameSite=Lax` par défaut | `defaultCookieOptions` (`cookie.ts:48`) |
577
+ | Requête inter-sites | `SameSite=Lax` par défaut | `cookieDefaultSettings` (`cookie.ts:39`) |
573
578
  | Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts:177`) |
574
- | Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts:226`) |
575
- | Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts:381`) |
576
- | Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts:394`) |
577
- | Résurrection après révocation | pierre tombale 5 min sur `write` **et** `touch` | `RevocationGuardStorage.ts:121` |
579
+ | Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts:231`) |
580
+ | Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts:388`) |
581
+ | Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts:401`) |
582
+ | Résurrection après révocation | pierre tombale 5 min sur `write` **et** `touch` | `RevocationGuardStorage.ts:127-171` |
578
583
  | Fuite d'identifiant en admin | `ref` HMAC + projection en liste blanche | `toSessionSummary()` (`sessions-service.ts:112`) |
579
- | IDOR sur « mes sessions » | périmètre depuis l'identité ALS, jamais du client | `destroyOwnByRef()` (`sessions-service.ts:834`) |
584
+ | IDOR sur « mes sessions » | périmètre depuis l'identité ALS, jamais du client | `destroyOwnByRef()` (`sessions-service.ts:872`) |
580
585
 
581
586
  ## 🧰 API publique
582
587
 
583
588
  Les signatures vivent dans `.ai/symbols.json` (jamais recopiées ici). Voici les usages réels.
584
589
 
585
- **Depuis un contrôleur** — `this.session` est un getter sur le contexte (`Controller.ts:279`) ; un
590
+ **Depuis un contrôleur** — `this.session` est un getter sur le contexte (`Controller.ts:384`) ; un
586
591
  paramètre `@Session()` suffit à déclarer l'intent.
587
592
 
588
593
  | Besoin | Appel | Effet |
@@ -590,7 +595,7 @@ paramètre `@Session()` suffit à déclarer l'intent.
590
595
  | Lire une valeur | `session.get("panier")` | `null` si absente — jamais `undefined`. |
591
596
  | Écrire une valeur | `session.set("panier", items)` | Marque la session `dirty` → écriture en fin de requête. |
592
597
  | Message « une seule lecture » | `session.setFlashBag("notice", "…")` | Consommé (et effacé) au premier `getFlashBag`. |
593
- | Lire ce message | `session.getFlashBag("notice")` | Rend la valeur puis la supprime (`session.ts:518`). |
598
+ | Lire ce message | `session.getFlashBag("notice")` | Rend la valeur puis la supprime (`session.ts:530`). |
594
599
  | Métadonnée technique | `session.getMetaBag("ip")` | ip / ua / host / remoteAddress posés à la création. |
595
600
  | Se déconnecter | `await session.destroy(true)` | Détruit l'entrée store **et** efface le cookie. |
596
601
  | Renouveler l'identifiant | `session.regenerateId()` | Nouvel identifiant, état conservé (`session.ts:236`). |
@@ -598,12 +603,12 @@ paramètre `@Session()` suffit à déclarer l'intent.
598
603
 
599
604
  **Intent de route** — `UseSession(options)` (`routerDecorators.ts:800`) s'applique à une classe **ou** à
600
605
  une méthode ; la méthode l'emporte, par fusion et non par remplacement
601
- (`resolveSessionIntent()`, `routerDecorators.ts:833`). **Une seule** option (`SessionIntent`,
606
+ (`resolveSessionIntent()`, `routerDecorators.ts:858`). **Une seule** option (`SessionIntent`,
602
607
  `ISession.ts:17`) :
603
608
 
604
609
  - `readOnly: true` — la session est reprise et lue mais **jamais** persistée ; une mutation tentée est
605
610
  journalisée en WARNING sans écriture (`Session.save()`, `session.ts:255-264`). C'est le seul champ
606
- propagé par le kernel (`http-kernel.ts:1194`).
611
+ propagé par le kernel (`http-kernel.ts:1210`).
607
612
 
608
613
  En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`routerDecorators.ts:189`).
609
614
 
@@ -611,7 +616,7 @@ En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`ro
611
616
 
612
617
  Le registre est une inversion de contrôle complète : `@nodefony/http` ne connaît **aucun** backend.
613
618
  Chaque module fournisseur se déclare lui-même au chargement, par
614
- `SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts:184`) — exactement ce que fait la
619
+ `SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts:196`) — exactement ce que fait la
615
620
  dernière ligne de chaque adaptateur (`@nodefony/redis/nodefony/src/SessionStorage.ts:323`).
616
621
 
617
622
  Pour ajouter un backend :
@@ -651,7 +656,7 @@ Trois règles de conception se dégagent du contrat, et méritent d'être respec
651
656
  Le coût d'une session est **payé seulement quand elle sert** :
652
657
 
653
658
  - **Zéro par défaut** — sans intent ni cookie, `startSession()` sort immédiatement
654
- (`http-kernel.ts:1152`) : ni objet `Session`, ni lecture de store.
659
+ (`http-kernel.ts:1168`) : ni objet `Session`, ni lecture de store.
655
660
  - **Objet léger** — trois sacs `{}` à plat, pas de container DI par session (`session.ts:100-104`).
656
661
  - **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts:255`) ; le
657
662
  `touch` est throttlé à une écriture par demi-vie d'idle (`session.ts:445`).
@@ -659,7 +664,7 @@ Le coût d'une session est **payé seulement quand elle sert** :
659
664
  probabiliste hérité de PHP (`sessions-service.ts:306-312`).
660
665
  - **Révocation quasi gratuite** — la `Map` de pierres tombales est **paresseuse** : sans révocation,
661
666
  `write` ne paie qu'une comparaison `=== null`, sans même un `Date.now()`
662
- (`RevocationGuardStorage.ts:163-168`).
667
+ (`RevocationGuardStorage.ts:181-186`).
663
668
  - **Administration bornée** — jamais plus de `SCAN_PAGE = 200` enregistrements en mémoire
664
669
  (`sessions-service.ts:75`), garde-fou à 5 000 pages (`sessions-service.ts:83`), parcours interrompu
665
670
  **journalisé**.
@@ -706,7 +711,7 @@ une révocation sans effet, **401** sur `mine` sans identité.
706
711
  **Écrans** — la page **Sessions** (`/nodefony/sessions`) de Studio liste les sessions vivantes par `ref` et permet la
707
712
  révocation unitaire ou en masse (`@nodefony/studio/frontend/src/routes/sessions/`). L'écran **Stores**
708
713
  affiche le backend réellement résolu, sa provenance et son emplacement physique : ces informations sont
709
- publiées au boot par `registerStoreResolution()` (`sessions-service.ts:290`), avec le chemin du fichier
714
+ publiées au boot par `registerStoreResolution()` (`sessions-service.ts:321`), avec le chemin du fichier
710
715
  SQLite quand c'est pertinent (`SessionStorage.location`,
711
716
  `@nodefony/drizzle/nodefony/src/SessionStorage.ts:55`).
712
717
 
@@ -714,7 +719,7 @@ SQLite quand c'est pertinent (`SessionStorage.location`,
714
719
 
715
720
  | Symptôme | Cause | Correction |
716
721
  | ------------------------------------------------------ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
717
- | Aucun `Set-Cookie`, `session` toujours vide | La route n'a **aucun intent** : ni `@UseSession`, ni paramètre `@Session` | Ajouter l'un des deux — l'activation est paresseuse (`http-kernel.ts:1142`) |
722
+ | Aucun `Set-Cookie`, `session` toujours vide | La route n'a **aucun intent** : ni `@UseSession`, ni paramètre `@Session` | Ajouter l'un des deux — l'activation est paresseuse (`http-kernel.ts:1210`) |
718
723
  | `context.session` est `null` dans un contrôleur WS | Intent posé sur la classe au lieu de la route, ou absent | `@UseSession()` **sur la route** WebSocket ; `Controller.startSession()` n'existe plus |
719
724
  | Mutation ignorée, WARNING « READONLY SESSION mutated » | La route est en `@UseSession({ readOnly: true })` | Retirer `readOnly` sur les routes qui écrivent (`session.ts:255-264`) |
720
725
  | Session détruite qui « revient » après logout | Une requête en vol réécrit le blob supprimé | Déjà couvert : pierre tombale 5 min (`RevocationGuardStorage.ts:121`) |