@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.
- package/README.md +7 -0
- package/dist/index.js +4 -2
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -1
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +22 -1
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +25 -13
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +340 -163
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +20 -20
- package/dist/nodefony/service/servers/server-https.js +38 -39
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
- package/dist/nodefony/service/servers/server-websocket.js +8 -7
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +19 -12
- package/dist/nodefony/service/trace.js +1 -1
- package/dist/nodefony/service/upload/upload-service.js +8 -7
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +93 -44
- package/dist/nodefony/src/context/http/Response.js +107 -51
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +8 -21
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +10 -10
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +20 -20
- package/docs/observabilite.md +25 -25
- package/docs/rate-limit.md +18 -18
- package/docs/servers.md +52 -52
- package/docs/session.md +65 -60
- package/docs/upload.md +36 -30
- 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
331
|
-
consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
428
|
-
| ---------------------------- | ----- |
|
|
429
|
-
| `maxHeadersCount` | int | `2000`
|
|
430
|
-
| `keepAliveTimeout` | ms | `5000`
|
|
431
|
-
| `timeout` | ms | `
|
|
432
|
-
| `requestTimeout` | ms | `30000`
|
|
433
|
-
| `responseTimeout` | ms | `30000`
|
|
434
|
-
| `shutdownTimeout` | ms | `5000`
|
|
435
|
-
| `headers` | objet | `null`
|
|
436
|
-
| `rejectUnauthorized` (https) | bool | `false`
|
|
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:
|
|
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()` (`
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
581
|
-
fourni, elle n'entre jamais dans le processus (`certificates.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
662
|
-
réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:
|
|
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:
|
|
736
|
-
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
838
|
-
| HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
73
|
-
(`http-kernel.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
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` (`
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
443
|
-
`
|
|
444
|
-
|
|
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
|
-
|
|
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:
|
|
456
|
-
**pierre tombale** de 5 minutes (`RevocationGuardStorage.ts:
|
|
457
|
-
(`RevocationGuardStorage.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
539
|
-
| Révocation par un admin | `destroyByRef()` (`sessions-service.ts:
|
|
540
|
-
| « Déconnecter partout » | `destroyByUser()` (`sessions-service.ts:
|
|
541
|
-
| « Mes appareils » (self) | `destroyOwnByRef()` (`sessions-service.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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 | `
|
|
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:
|
|
575
|
-
| Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts:
|
|
576
|
-
| Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts:
|
|
577
|
-
| Résurrection après révocation | pierre tombale 5 min sur `write` **et** `touch` | `RevocationGuardStorage.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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`) |
|