@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +13 -10
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +3 -3
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
- package/dist/nodefony/service/audit-logger.js +23 -30
- package/dist/nodefony/service/certificates.js +97 -13
- package/dist/nodefony/service/error-renderer.js +29 -12
- package/dist/nodefony/service/http-kernel.js +198 -50
- package/dist/nodefony/service/pretty-request-logger.js +15 -13
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +3 -3
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
- package/dist/nodefony/service/servers/server-websocket.js +10 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
- package/dist/nodefony/service/sessions/sessions-service.js +7 -14
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +78 -43
- package/dist/nodefony/src/context/domainMatcher.js +1 -2
- package/dist/nodefony/src/context/forwarded.js +1 -2
- package/dist/nodefony/src/context/http/HttpContext.js +28 -6
- package/dist/nodefony/src/context/http/Request.js +33 -29
- package/dist/nodefony/src/context/http/Response.js +23 -6
- package/dist/nodefony/src/context/http/SseStream.js +1 -2
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +41 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/responseEnded.js +1 -2
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +33 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
- package/dist/nodefony/src/profiler/Profiler.js +104 -32
- package/dist/nodefony/src/profiler/aggregate.js +442 -0
- package/dist/nodefony/src/proxy/forward.js +29 -5
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +12 -6
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
- package/dist/nodefony/src/session/session.js +2 -3
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +9 -3
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/certificates.d.ts +55 -4
- package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/src/context/Context.d.ts +38 -5
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
- package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
- package/docs/cookies.md +85 -78
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +6 -1
- package/docs/observabilite.md +159 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +72 -70
- package/docs/servers.md +338 -224
- package/docs/session.md +179 -179
- package/docs/sse.md +24 -20
- package/docs/upload.md +120 -117
- package/package.json +8 -9
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
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#HttpKernel.startSession`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
|
|
73
|
+
(`http-kernel.ts#HttpKernel.publishRuntimePorts`) — sans intent de route ni cookie entrant, **aucune session n'est ouverte**.
|
|
74
74
|
|
|
75
75
|
## 📖 Lexique
|
|
76
76
|
|
|
@@ -114,15 +114,15 @@ défaut** dans Nodefony :
|
|
|
114
114
|
|
|
115
115
|
- **Vol du cookie (hijacking).** Un script injecté (XSS) ou un réseau en clair capte le cookie et
|
|
116
116
|
rejoue la session. → `HttpOnly` et `Secure` sont à `true` par défaut (`sessionCookieSchema`,
|
|
117
|
-
`config.ts
|
|
118
|
-
(`Context.getSessionCookieName()`, `Context.ts
|
|
117
|
+
`config.ts#sessionCookieSchema`), et le nom du cookie prend le préfixe `__Host-` dès que le transport est TLS
|
|
118
|
+
(`Context.getSessionCookieName()`, `Context.ts#Context.getSessionCookieName`).
|
|
119
119
|
- **Fixation.** L'attaquant pose lui-même un identifiant dans le navigateur de la victime, attend
|
|
120
120
|
qu'elle se connecte, puis réutilise **le même** identifiant. → double défense : `strictMode` rejette
|
|
121
|
-
tout identifiant inconnu du store (`Session.resume()`, `session.ts
|
|
121
|
+
tout identifiant inconnu du store (`Session.resume()`, `session.ts#Session.resume`), et le login **régénère**
|
|
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#Session.isValidSession`), 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
|
|
@@ -134,21 +134,21 @@ défaut** dans Nodefony :
|
|
|
134
134
|
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
|
-
cookie, sans déchiffrement (`session.ts
|
|
138
|
-
(`session.ts
|
|
137
|
+
cookie, sans déchiffrement (`session.ts#Session.getSession`) ; l'identifiant vient de `Session.generateId()`
|
|
138
|
+
(`session.ts#Session.generateId`). 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#HttpKernel.startSession`). 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
|
|
148
|
-
l'activité HTTP **ou** WS prolonge la même session (`Session.touchIfNeeded()`, `session.ts
|
|
147
|
+
`HttpKernel.onRequestEnd()` (`http-kernel.ts#HttpKernel.onRequestEnd`) et `HttpKernel.onConnect()` (`http-kernel.ts#HttpKernel.onConnect`) ;
|
|
148
|
+
l'activité HTTP **ou** WS prolonge la même session (`Session.touchIfNeeded()`, `session.ts#Session.touchIfNeeded`).
|
|
149
149
|
|
|
150
150
|
**4. L'administration ne voit jamais un identifiant.** Un opérateur manipule une `ref`, HMAC tronqué
|
|
151
|
-
non réversible produit par `computeSessionRef()` (`sessions-service.ts
|
|
151
|
+
non réversible produit par `computeSessionRef()` (`sessions-service.ts#computeSessionRef`) — comme la liste
|
|
152
152
|
« appareils connectés » de GitHub ou Google montre une référence, jamais le jeton.
|
|
153
153
|
|
|
154
154
|
Compromis assumé : l'état serveur suppose un store **partagé** dès qu'on passe à plusieurs pods
|
|
@@ -274,31 +274,31 @@ curl -sk -b /tmp/jar https://localhost:5152/panier
|
|
|
274
274
|
Le cookie obtenu porte `__Host-`, `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/` et **aucun** `Domain` :
|
|
275
275
|
c'est exactement ce qu'assert le banc d'intégration `session-runtime` (« Set-Cookie de session sur TLS »).
|
|
276
276
|
Sur un transport en clair (port 5151), le préfixe `__Host-` est omis — le navigateur le rejetterait
|
|
277
|
-
faute de `Secure` (`Context.getSessionCookieName()`, `Context.ts
|
|
277
|
+
faute de `Secure` (`Context.getSessionCookieName()`, `Context.ts#Context.getSessionCookieName`).
|
|
278
278
|
|
|
279
279
|
## ⚙️ Configuration
|
|
280
280
|
|
|
281
|
-
Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts
|
|
282
|
-
`sessionCookieSchema` (`config.ts
|
|
283
|
-
|
|
284
|
-
| Option | Type | Défaut | Effet
|
|
285
|
-
| ------------------- | ------- | ------------ |
|
|
286
|
-
| `store` | string | `"auto"` | Backend de persistance — voir la résolution ci-dessous (`config.ts
|
|
287
|
-
| `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts
|
|
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
|
|
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).
|
|
281
|
+
Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts#sessionSchema`) et son sous-schéma
|
|
282
|
+
`sessionCookieSchema` (`config.ts#sessionCookieSchema`).
|
|
283
|
+
|
|
284
|
+
| Option | Type | Défaut | Effet |
|
|
285
|
+
| ------------------- | ------- | ------------ | --------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `store` | string | `"auto"` | Backend de persistance — voir la résolution ci-dessous (`config.ts#sessionCookieSchema`). |
|
|
287
|
+
| `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts#sessionCookieSchema`). |
|
|
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#sessionSchema`). |
|
|
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` (`cookieDefaultSettings`, `cookie.ts
|
|
301
|
+
`Lax` (`cookieDefaultSettings`, `cookie.ts#cookieDefaultSettings`).
|
|
302
302
|
|
|
303
303
|
> [!WARNING]
|
|
304
304
|
> `idleTimeoutS: 0` **et** `absoluteTimeoutS: 0` désactivent les deux bornes : une session ne meurt
|
|
@@ -309,8 +309,8 @@ Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:803`) et
|
|
|
309
309
|
### Comment `store: "auto"` se résout au boot
|
|
310
310
|
|
|
311
311
|
`auto` n'est pas un store : c'est une sentinelle résolue une fois, au boot, par `resolveAutoStore()`
|
|
312
|
-
(`config/infra.ts
|
|
313
|
-
réellement enregistrés (`SessionsService.initializeStorage()`, `sessions-service.ts
|
|
312
|
+
(`config/infra.ts#resolveAutoStore`), 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#SessionsService.initializeStorage`).
|
|
314
314
|
|
|
315
315
|
```mermaid
|
|
316
316
|
flowchart TD
|
|
@@ -331,13 +331,13 @@ Deux comportements à connaître :
|
|
|
331
331
|
|
|
332
332
|
- **Sans aucune infra déclarée, on ne tombe pas en `memory`** : si `@nodefony/drizzle` est chargé, la
|
|
333
333
|
session persiste en SQLite local — tes données survivent au redémarrage sans une ligne de config
|
|
334
|
-
(`infra.ts
|
|
334
|
+
(`infra.ts#CONNECTED_STORES`).
|
|
335
335
|
- **Un `store` explicite inconnu ne dégrade pas en silence** : en production le boot est **avorté**, en
|
|
336
|
-
développement il y a repli `memory` **annoncé** en WARNING (`sessions-service.ts
|
|
336
|
+
développement il y a repli `memory` **annoncé** en WARNING (`sessions-service.ts#SessionsService`).
|
|
337
337
|
|
|
338
338
|
## 🗂️ Choisir son store
|
|
339
339
|
|
|
340
|
-
Le contrat est unique — `ISessionStorage` (`ISession.ts
|
|
340
|
+
Le contrat est unique — `ISessionStorage` (`ISession.ts#ISessionStorage`) — et **tous** les backends le portent.
|
|
341
341
|
Ce qui change, c'est la topologie et la façon d'expirer.
|
|
342
342
|
|
|
343
343
|
| Store | Où vit l'état | Multi-pod | Expiration idle | `total` admin | Quand le choisir |
|
|
@@ -351,53 +351,53 @@ 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`). 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
|
|
|
358
358
|
Il porte quand même **toute** la sémantique du contrat : `createdAt` figé à la création, `updatedAt`
|
|
359
|
-
rafraîchi par `MemorySessionStorage.touch()` (`MemorySessionStorage.ts
|
|
360
|
-
par `gc(idleSeconds, absoluteSeconds)` (`MemorySessionStorage.ts
|
|
361
|
-
et tri déterministe (`MemorySessionStorage.listPage()`, `MemorySessionStorage.ts
|
|
359
|
+
rafraîchi par `MemorySessionStorage.touch()` (`MemorySessionStorage.ts#MemorySessionStorage.touch`), purge sur les deux bornes
|
|
360
|
+
par `gc(idleSeconds, absoluteSeconds)` (`MemorySessionStorage.ts#MemorySessionStorage.touch`), pagination offset à `total` exact
|
|
361
|
+
et tri déterministe (`MemorySessionStorage.listPage()`, `MemorySessionStorage.ts#MemorySessionStorage.listPage`).
|
|
362
362
|
|
|
363
363
|
### `drizzle` — SQL, le défaut persistant
|
|
364
364
|
|
|
365
365
|
Une table `session`, une ligne par session, écrite en **UPSERT atomique** (`INSERT … ON CONFLICT DO
|
|
366
366
|
UPDATE … RETURNING`) : une seule requête, aucune course entre insertion et mise à jour
|
|
367
|
-
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
367
|
+
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.write`). Le `touch` est un simple
|
|
368
368
|
`UPDATE updatedAt` sur la clé primaire, sans réécrire le blob
|
|
369
|
-
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
369
|
+
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.gc`).
|
|
370
370
|
|
|
371
371
|
Le GC supprime en **deux `DELETE` distincts** — idle puis absolute — plutôt qu'un `$or`, pour rester
|
|
372
372
|
portable sur tous les adaptateurs `orm-core`
|
|
373
|
-
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
373
|
+
(`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage`). La pagination est native
|
|
374
374
|
(`LIMIT`/`OFFSET` + `COUNT`), ordonnée `updatedAt DESC` puis `session_id ASC` pour rester déterministe
|
|
375
|
-
à horodatage égal (`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
375
|
+
à horodatage égal (`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.#toRecord`).
|
|
376
376
|
|
|
377
377
|
Détail à connaître : une session anonyme est stockée `user = NULL`, pas chaîne vide — le filtre le
|
|
378
|
-
traduit (`$null`) au lieu de chercher `""` (`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
378
|
+
traduit (`$null`) au lieu de chercher `""` (`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.#toRecord`).
|
|
379
379
|
|
|
380
380
|
### `redis` — TTL natif, zéro balayage
|
|
381
381
|
|
|
382
382
|
Ici l'expiration **idle** est portée par Redis lui-même : `SET … EX` pose le TTL à chaque écriture
|
|
383
|
-
(`@nodefony/redis/nodefony/src/SessionStorage.ts
|
|
384
|
-
(`@nodefony/redis/nodefony/src/SessionStorage.ts
|
|
385
|
-
(`@nodefony/redis/nodefony/src/SessionStorage.ts
|
|
383
|
+
(`@nodefony/redis/nodefony/src/SessionStorage.ts#RedisSessionStorage.touch`) et `touch` le repositionne par un `EXPIRE` O(1)
|
|
384
|
+
(`@nodefony/redis/nodefony/src/SessionStorage.ts#RedisSessionStorage.write`). Conséquence : `gc()` est un **no-op assumé**
|
|
385
|
+
(`@nodefony/redis/nodefony/src/SessionStorage.ts#RedisSessionStorage.write`) — 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#Session.isValidSession`). 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
|
|
392
392
|
d'ordre global) et `countSessions()` renvoie **`-1`** = « je ne sais pas »
|
|
393
|
-
(`@nodefony/redis/nodefony/src/SessionStorage.ts
|
|
393
|
+
(`@nodefony/redis/nodefony/src/SessionStorage.ts#RedisSessionStorage.countSessions`). L'appelant affiche l'inconnu, il ne l'invente pas.
|
|
394
394
|
|
|
395
395
|
### `mongoose` — MongoDB, parité de comportement
|
|
396
396
|
|
|
397
397
|
Même sémantique que le store SQL : `findOneAndUpdate({ upsert: true })` en une passe
|
|
398
|
-
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts
|
|
399
|
-
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts
|
|
400
|
-
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts
|
|
398
|
+
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts#SessionStorage.write`), `touch` en `updateOne`
|
|
399
|
+
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts#SessionStorage.gc`), GC en deux suppressions `$lt`
|
|
400
|
+
(`@nodefony/mongoose/nodefony/src/SessionStorage.ts#SessionStorage`). Les horodatages sont des **nombres**
|
|
401
401
|
(epoch ms) et non des `Date` Mongo, précisément pour que le store reste interchangeable avec Drizzle.
|
|
402
402
|
|
|
403
403
|
> [!TIP]
|
|
@@ -435,12 +435,12 @@ sequenceDiagram
|
|
|
435
435
|
end
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
**Reprise ou création.** `Session.start()` (`session.ts
|
|
439
|
-
→ `resume()` (`session.ts
|
|
438
|
+
**Reprise ou création.** `Session.start()` (`session.ts#Session.start`) délègue à `getSession()` : cookie présent
|
|
439
|
+
→ `resume()` (`session.ts#Session.resume`), sinon `create()` (`session.ts#Session.create`) 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
|
-
(âge depuis `created`) puis l'**idle** (depuis `updated`). Échec → `invalidate()` (`session.ts
|
|
442
|
+
**Validation à la reprise.** `Session.isValidSession()` (`session.ts#Session.isValidSession`) applique l'**absolute**
|
|
443
|
+
(âge depuis `created`) puis l'**idle** (depuis `updated`). Échec → `invalidate()` (`session.ts#Session.invalidate`)
|
|
444
444
|
détruit l'entrée et recrée une session vierge.
|
|
445
445
|
|
|
446
446
|
> [!WARNING]
|
|
@@ -449,28 +449,28 @@ détruit l'entrée et recrée une session vierge.
|
|
|
449
449
|
> **idle** ne sont alors **pas** évaluées. Tant que ce comportement n'est pas corrigé, ne pas
|
|
450
450
|
> activer `refererCheck` quand l'expiration des sessions compte — c'est-à-dire presque toujours.
|
|
451
451
|
|
|
452
|
-
**Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts
|
|
453
|
-
session est `dirty` et non `readOnly` ; sinon il appelle `Session.touchIfNeeded()` (`session.ts
|
|
452
|
+
**Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts#SessionsService.saveSession`) n'écrit **que** si la
|
|
453
|
+
session est `dirty` et non `readOnly` ; sinon il appelle `Session.touchIfNeeded()` (`session.ts#Session.touchIfNeeded`),
|
|
454
454
|
qui prolonge l'idle **sans réécrire le blob**, et seulement au-delà d'une demi-vie d'idle
|
|
455
|
-
(`session.ts
|
|
455
|
+
(`session.ts#Session.touchIfNeeded`). Une requête de lecture coûte donc au pire un `UPDATE` d'horodatage toutes les
|
|
456
456
|
15 minutes (défaut).
|
|
457
457
|
|
|
458
|
-
**Anti-résurrection**, sur deux niveaux. `Session.destroy()` remet `mutated = false` (`session.ts
|
|
458
|
+
**Anti-résurrection**, sur deux niveaux. `Session.destroy()` remet `mutated = false` (`session.ts#Session.invalidate`)
|
|
459
459
|
pour que la sauvegarde de fin de requête ne réécrive pas ce qu'on vient de supprimer. Surtout, **tout**
|
|
460
|
-
store est décoré par `RevocationGuardStorage` (`sessions-service.ts
|
|
461
|
-
**pierre tombale** de 5 minutes (`RevocationGuardStorage.ts
|
|
462
|
-
(`RevocationGuardStorage.ts
|
|
460
|
+
store est décoré par `RevocationGuardStorage` (`sessions-service.ts#SessionsService.initializeStorage`) : `destroy()` pose une
|
|
461
|
+
**pierre tombale** de 5 minutes (`RevocationGuardStorage.ts#RevocationGuardStorage.destroy`) qui refuse ensuite tout `write`
|
|
462
|
+
(`RevocationGuardStorage.ts#RevocationGuardStorage.write`) **et tout `touch`** (`RevocationGuardStorage.ts#RevocationGuardStorage.touch`) du même
|
|
463
463
|
identifiant — ce qui couvre la requête « en vol » d'un autre client.
|
|
464
464
|
|
|
465
465
|
**Purge hors requête.** Un `GcScheduler` est armé au `onReady`, désarmé au `onTerminate`
|
|
466
|
-
(`sessions-service.ts
|
|
467
|
-
(`sessions-service.ts
|
|
466
|
+
(`sessions-service.ts#SessionsService.initializeStorage`). La passe métier nue, `SessionsService.runGc()`
|
|
467
|
+
(`sessions-service.ts#SessionsService.runGc`), est publique exprès : un CronJob Kubernetes peut l'appeler à la place du
|
|
468
468
|
timer (`gcIntervalS: 0`).
|
|
469
469
|
|
|
470
470
|
## Entités de persistance
|
|
471
471
|
|
|
472
472
|
**Drizzle (SQL).** La table est décrite une seule fois en spec logique (`SESSION_TABLE_SPEC`,
|
|
473
|
-
`sessionEntity.ts
|
|
473
|
+
`sessionEntity.ts#SESSION_TABLE_SPEC`) et déclinée par dialecte par `buildFrameworkTable()` (`colKit.ts#buildFrameworkTable`) — mêmes **noms** de
|
|
474
474
|
colonnes partout, donc un store dialect-agnostique.
|
|
475
475
|
|
|
476
476
|
| Colonne | Type logique | SQLite | PostgreSQL | MySQL / MariaDB | Rôle |
|
|
@@ -485,15 +485,15 @@ colonnes partout, donc un store dialect-agnostique.
|
|
|
485
485
|
|
|
486
486
|
En MySQL/MariaDB, une colonne texte indexée devient `varchar` (un `TEXT` InnoDB n'est pas indexable
|
|
487
487
|
sans préfixe) et le type JSON passe par un type compatible qui tolère MariaDB, laquelle stocke le JSON
|
|
488
|
-
en `LONGTEXT` (`colKit.ts
|
|
488
|
+
en `LONGTEXT` (`colKit.ts#mysqlColumn`).
|
|
489
489
|
|
|
490
|
-
**Mongoose (MongoDB).** Schéma équivalent (`@nodefony/mongoose/nodefony/entity/sessionEntity.ts
|
|
490
|
+
**Mongoose (MongoDB).** Schéma équivalent (`@nodefony/mongoose/nodefony/entity/sessionEntity.ts#schema`) :
|
|
491
491
|
`session_id` (String, index **unique**), `Attributes`/`flashBag`/`metaBag` (Object, défaut `{}`), `user`
|
|
492
492
|
(String, défaut `null`), `createdAt`/`updatedAt` (Number, ms).
|
|
493
493
|
|
|
494
494
|
Le connecteur diffère volontairement entre les deux adaptateurs — `"default"` pour Drizzle
|
|
495
|
-
(`sessionEntity.ts
|
|
496
|
-
(`@nodefony/mongoose/nodefony/entity/sessionEntity.ts
|
|
495
|
+
(`sessionEntity.ts#schema`), `"nodefony"` pour Mongoose
|
|
496
|
+
(`@nodefony/mongoose/nodefony/entity/sessionEntity.ts#SESSION_CONNECTOR`) — parce que le registre d'entités est
|
|
497
497
|
partagé par processus : deux noms distincts évitent la collision quand les deux ORM cohabitent.
|
|
498
498
|
|
|
499
499
|
## 🔌 HTTP et WebSocket — la même session
|
|
@@ -503,17 +503,17 @@ C'est le différenciateur du framework appliqué à l'état de session : un seul
|
|
|
503
503
|
<!-- prettier-ignore -->
|
|
504
504
|
| Aspect | HTTP | WebSocket |
|
|
505
505
|
| --- | --- | --- |
|
|
506
|
-
| Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts
|
|
507
|
-
| Lecture du cookie | constructeur du contexte | constructeur, même nom effectif (`WebsocketContext.ts
|
|
508
|
-
| Sauvegarde | fin de requête | après **chaque frame** traitée (`WebsocketContext.ts
|
|
509
|
-
| Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts
|
|
510
|
-
| Portée ALS | une requête | **handshake + toutes les frames** (`http-kernel.ts
|
|
506
|
+
| Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts#HttpKernel.onRequestEnd`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts#HttpKernel.onConnect`) |
|
|
507
|
+
| Lecture du cookie | constructeur du contexte | constructeur, même nom effectif (`WebsocketContext.ts#WebsocketContext.constructor`) |
|
|
508
|
+
| Sauvegarde | fin de requête | après **chaque frame** traitée (`WebsocketContext.ts#WebsocketContext.handle`) |
|
|
509
|
+
| Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts#HttpKernel.runFirewall`) |
|
|
510
|
+
| Portée ALS | une requête | **handshake + toutes les frames** (`http-kernel.ts#HttpKernel.createHttpContext`) |
|
|
511
511
|
|
|
512
512
|
La conséquence pratique la plus utile : côté WebSocket, la bulle `AsyncLocalStorage` ouverte au
|
|
513
|
-
handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts
|
|
513
|
+
handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts#HttpKernel.configureRateLimit`). L'identité résolue une fois est donc
|
|
514
514
|
disponible à chaque frame sans relire la base — c'est ce dont profite
|
|
515
|
-
`FirewallRealtimeAuthenticator.supports()` (`FirewallRealtimeAuthenticator.ts
|
|
516
|
-
par le firewall sur les zones temps réel protégées (`firewall.ts
|
|
515
|
+
`FirewallRealtimeAuthenticator.supports()` (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.supports`), câblé automatiquement
|
|
516
|
+
par le firewall sur les zones temps réel protégées (`firewall.ts#Firewall.#build`).
|
|
517
517
|
|
|
518
518
|
> [!WARNING]
|
|
519
519
|
> Rien à écrire dans `initialize()` : il n'existe **pas** de `Controller.startSession()`. La session WS
|
|
@@ -526,34 +526,34 @@ par le firewall sur les zones temps réel protégées (`firewall.ts:298`).
|
|
|
526
526
|
### Régénération d'identifiant à la connexion (anti-fixation)
|
|
527
527
|
|
|
528
528
|
C'est la défense la plus importante et elle est **active**. `AuthFlow.#openSession()`
|
|
529
|
-
(`authFlow.ts
|
|
530
|
-
appel **inconditionnel** de `Session.regenerateId()` (`authFlow.ts
|
|
531
|
-
l'ancienne entrée du store (`authFlow.ts
|
|
529
|
+
(`authFlow.ts#AuthFlow.#openSession`) : reprise ou ouverture de la session, mémorisation de l'ancien identifiant, puis
|
|
530
|
+
appel **inconditionnel** de `Session.regenerateId()` (`authFlow.ts#AuthFlow.logout`), et enfin destruction de
|
|
531
|
+
l'ancienne entrée du store (`authFlow.ts#AuthFlow.logout`). Un cookie pré-posé par un attaquant **ne survit donc pas
|
|
532
532
|
au login**. Le nouvel identifiant est un CSPRNG frais, l'état applicatif est conservé
|
|
533
|
-
(`Session.regenerateId()`, `session.ts
|
|
533
|
+
(`Session.regenerateId()`, `session.ts#Session.regenerateId`).
|
|
534
534
|
|
|
535
|
-
Au passage, la provenance est capturée dans le `metaBag` : `ip` (`authFlow.ts
|
|
536
|
-
(`authFlow.ts
|
|
535
|
+
Au passage, la provenance est capturée dans le `metaBag` : `ip` (`authFlow.ts#AuthFlow.#openSession`) et `ua`
|
|
536
|
+
(`authFlow.ts#AuthFlow.logoutWithProvider`), en mode « au mieux » — ce sont ces deux champs que la console d'administration
|
|
537
537
|
affiche.
|
|
538
538
|
|
|
539
539
|
### Révocation — immédiate et par construction
|
|
540
540
|
|
|
541
|
-
| Surface | Méthode
|
|
542
|
-
| ------------------------ |
|
|
543
|
-
| Déconnexion locale | `Session.destroy()` (`session.ts
|
|
544
|
-
| Révocation par un admin | `destroyByRef()` (`sessions-service.ts
|
|
545
|
-
| « Déconnecter partout » | `destroyByUser()` (`sessions-service.ts
|
|
546
|
-
| « Mes appareils » (self) | `destroyOwnByRef()` (`sessions-service.ts
|
|
541
|
+
| Surface | Méthode | Portée |
|
|
542
|
+
| ------------------------ | --------------------------------------------------------------------------- | ------------------------------------------- |
|
|
543
|
+
| Déconnexion locale | `Session.destroy()` (`session.ts#Session.destroy`) | la session courante + pierre tombale |
|
|
544
|
+
| Révocation par un admin | `destroyByRef()` (`sessions-service.ts#SessionsService.destroyByRef`) | une session désignée par sa `ref` publique |
|
|
545
|
+
| « Déconnecter partout » | `destroyByUser()` (`sessions-service.ts#SessionsService.destroyByUser`) | toutes les sessions d'un utilisateur |
|
|
546
|
+
| « Mes appareils » (self) | `destroyOwnByRef()` (`sessions-service.ts#SessionsService.destroyOwnByRef`) | une session, **restreinte au propriétaire** |
|
|
547
547
|
|
|
548
548
|
Deux finesses valent d'être connues.
|
|
549
549
|
|
|
550
550
|
`destroyByUser()` ne fait pas un seul passage : il **repasse jusqu'à ce qu'un passage complet ne
|
|
551
|
-
détruise plus rien** (`sessions-service.ts
|
|
551
|
+
détruise plus rien** (`sessions-service.ts#SessionsService.destroyByUser`), car supprimer en parcourant décale les rangs sous un
|
|
552
552
|
curseur offset. Une révocation « partout » qui en laisserait une n'est pas une imprécision, c'est une
|
|
553
553
|
faille — on rend donc la main avec la preuve, pas l'espoir.
|
|
554
554
|
|
|
555
555
|
`destroyOwnByRef()` ferme l'IDOR **par construction** : parcours restreint aux sessions du demandeur,
|
|
556
|
-
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#SessionsService.destroyOwnByRef`). Une
|
|
557
557
|
`ref` d'autrui est structurellement introuvable.
|
|
558
558
|
|
|
559
559
|
### Redaction — l'identifiant ne sort jamais du process
|
|
@@ -561,70 +561,70 @@ et appartenance **re-vérifiée** avant même de comparer la `ref` (`sessions-se
|
|
|
561
561
|
Trois barrières superposées :
|
|
562
562
|
|
|
563
563
|
1. Le contrat impose que `listPage()` rende `Attributes` et `flashBag` **vides**
|
|
564
|
-
(`ISession.ts
|
|
565
|
-
2. La projection vers l'extérieur passe par `toSessionSummary()` (`sessions-service.ts
|
|
564
|
+
(`ISession.ts#ISessionStorage.listPage`) — les stores SQL/NoSQL ne les sélectionnent même pas.
|
|
565
|
+
2. La projection vers l'extérieur passe par `toSessionSummary()` (`sessions-service.ts#toSessionSummary`), bâtie en
|
|
566
566
|
**liste blanche** : `ref`, `user`, `authenticated`, `ip`, `ua`, dates. Jamais un `delete` après coup.
|
|
567
567
|
3. La `ref` elle-même est un HMAC tronqué non réversible (`computeSessionRef()`,
|
|
568
|
-
`sessions-service.ts
|
|
569
|
-
(`SessionsService.sessionRef()`, `sessions-service.ts
|
|
568
|
+
`sessions-service.ts#computeSessionRef`) ; la clé est dérivée du certificat au boot et n'est jamais sérialisée
|
|
569
|
+
(`SessionsService.sessionRef()`, `sessions-service.ts#SessionsService.sessionRef`).
|
|
570
570
|
|
|
571
571
|
### Récapitulatif des défenses actives par défaut
|
|
572
572
|
|
|
573
|
-
| Menace | Défense | Ancrage
|
|
574
|
-
| --------------------------------- | ------------------------------------------------- |
|
|
575
|
-
| Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts
|
|
576
|
-
| Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts
|
|
577
|
-
| Requête inter-sites | `SameSite=Lax` par défaut | `cookieDefaultSettings` (`cookie.ts
|
|
578
|
-
| Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts
|
|
579
|
-
| Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts
|
|
580
|
-
| Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts
|
|
581
|
-
| Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts
|
|
582
|
-
| Résurrection après révocation | pierre tombale 5 min sur `write` **et** `touch` | `RevocationGuardStorage.ts
|
|
583
|
-
| Fuite d'identifiant en admin | `ref` HMAC + projection en liste blanche | `toSessionSummary()` (`sessions-service.ts
|
|
584
|
-
| IDOR sur « mes sessions » | périmètre depuis l'identité ALS, jamais du client | `destroyOwnByRef()` (`sessions-service.ts
|
|
573
|
+
| Menace | Défense | Ancrage |
|
|
574
|
+
| --------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
575
|
+
| Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts#sessionCookieSchema`) |
|
|
576
|
+
| Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts#Context.getSessionCookieName`) |
|
|
577
|
+
| Requête inter-sites | `SameSite=Lax` par défaut | `cookieDefaultSettings` (`cookie.ts#cookieDefaultSettings`) |
|
|
578
|
+
| Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts#Session.resume`) |
|
|
579
|
+
| Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts#Session.generateId`) |
|
|
580
|
+
| Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts#Session.isValidSession`) |
|
|
581
|
+
| Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts#Session.isValidSession`) |
|
|
582
|
+
| Résurrection après révocation | pierre tombale 5 min sur `write` **et** `touch` | `RevocationGuardStorage.ts#RevocationGuardStorage.touch` |
|
|
583
|
+
| Fuite d'identifiant en admin | `ref` HMAC + projection en liste blanche | `toSessionSummary()` (`sessions-service.ts#toSessionSummary`) |
|
|
584
|
+
| IDOR sur « mes sessions » | périmètre depuis l'identité ALS, jamais du client | `destroyOwnByRef()` (`sessions-service.ts#SessionsService.destroyOwnByRef`) |
|
|
585
585
|
|
|
586
586
|
## 🧰 API publique
|
|
587
587
|
|
|
588
588
|
Les signatures vivent dans `.ai/symbols.json` (jamais recopiées ici). Voici les usages réels.
|
|
589
589
|
|
|
590
|
-
**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#Controller.session`) ; un
|
|
591
591
|
paramètre `@Session()` suffit à déclarer l'intent.
|
|
592
592
|
|
|
593
|
-
| Besoin | Appel | Effet
|
|
594
|
-
| -------------------------------- | ------------------------------------ |
|
|
595
|
-
| Lire une valeur | `session.get("panier")` | `null` si absente — jamais `undefined`.
|
|
596
|
-
| Écrire une valeur | `session.set("panier", items)` | Marque la session `dirty` → écriture en fin de requête.
|
|
597
|
-
| Message « une seule lecture » | `session.setFlashBag("notice", "…")` | Consommé (et effacé) au premier `getFlashBag`.
|
|
598
|
-
| Lire ce message | `session.getFlashBag("notice")` | Rend la valeur puis la supprime (`session.ts
|
|
599
|
-
| Métadonnée technique | `session.getMetaBag("ip")` | ip / ua / host / remoteAddress posés à la création.
|
|
600
|
-
| Se déconnecter | `await session.destroy(true)` | Détruit l'entrée store **et** efface le cookie.
|
|
601
|
-
| Renouveler l'identifiant | `session.regenerateId()` | Nouvel identifiant, état conservé (`session.ts
|
|
602
|
-
| Savoir si une écriture aura lieu | `session.dirty` | Le drapeau de dirty-tracking (`session.ts
|
|
603
|
-
|
|
604
|
-
**Intent de route** — `UseSession(options)` (`routerDecorators.ts
|
|
593
|
+
| Besoin | Appel | Effet |
|
|
594
|
+
| -------------------------------- | ------------------------------------ | ---------------------------------------------------------------------- |
|
|
595
|
+
| Lire une valeur | `session.get("panier")` | `null` si absente — jamais `undefined`. |
|
|
596
|
+
| Écrire une valeur | `session.set("panier", items)` | Marque la session `dirty` → écriture en fin de requête. |
|
|
597
|
+
| Message « une seule lecture » | `session.setFlashBag("notice", "…")` | Consommé (et effacé) au premier `getFlashBag`. |
|
|
598
|
+
| Lire ce message | `session.getFlashBag("notice")` | Rend la valeur puis la supprime (`session.ts#Session.getFlashBag`). |
|
|
599
|
+
| Métadonnée technique | `session.getMetaBag("ip")` | ip / ua / host / remoteAddress posés à la création. |
|
|
600
|
+
| Se déconnecter | `await session.destroy(true)` | Détruit l'entrée store **et** efface le cookie. |
|
|
601
|
+
| Renouveler l'identifiant | `session.regenerateId()` | Nouvel identifiant, état conservé (`session.ts#Session.regenerateId`). |
|
|
602
|
+
| Savoir si une écriture aura lieu | `session.dirty` | Le drapeau de dirty-tracking (`session.ts#Session.dirty`). |
|
|
603
|
+
|
|
604
|
+
**Intent de route** — `UseSession(options)` (`routerDecorators.ts#UseSession`) s'applique à une classe **ou** à
|
|
605
605
|
une méthode ; la méthode l'emporte, par fusion et non par remplacement
|
|
606
|
-
(`resolveSessionIntent()`, `routerDecorators.ts
|
|
607
|
-
`ISession.ts
|
|
606
|
+
(`resolveSessionIntent()`, `routerDecorators.ts#resolveSessionIntent`). **Une seule** option (`SessionIntent`,
|
|
607
|
+
`ISession.ts#SessionIntent`) :
|
|
608
608
|
|
|
609
609
|
- `readOnly: true` — la session est reprise et lue mais **jamais** persistée ; une mutation tentée est
|
|
610
|
-
journalisée en WARNING sans écriture (`Session.save()`, `session.ts
|
|
611
|
-
propagé par le kernel (`http-kernel.ts
|
|
610
|
+
journalisée en WARNING sans écriture (`Session.save()`, `session.ts#Session.save`). C'est le seul champ
|
|
611
|
+
propagé par le kernel (`http-kernel.ts#HttpKernel.initServers`).
|
|
612
612
|
|
|
613
|
-
En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`routerDecorators.ts
|
|
613
|
+
En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`routerDecorators.ts#controller`).
|
|
614
614
|
|
|
615
615
|
## 🧩 Extension — brancher son propre store
|
|
616
616
|
|
|
617
617
|
Le registre est une inversion de contrôle complète : `@nodefony/http` ne connaît **aucun** backend.
|
|
618
618
|
Chaque module fournisseur se déclare lui-même au chargement, par
|
|
619
|
-
`SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts
|
|
620
|
-
dernière ligne de chaque adaptateur (`@nodefony/redis/nodefony/src/SessionStorage.ts
|
|
619
|
+
`SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts#SessionsService.registerStorage`) — exactement ce que fait la
|
|
620
|
+
dernière ligne de chaque adaptateur (`@nodefony/redis/nodefony/src/SessionStorage.ts#RedisSessionStorage.listAll`).
|
|
621
621
|
|
|
622
622
|
Pour ajouter un backend :
|
|
623
623
|
|
|
624
|
-
1. Implémenter `ISessionStorage` (`ISession.ts
|
|
624
|
+
1. Implémenter `ISessionStorage` (`ISession.ts#ISessionStorage`). Le **noyau obligatoire** est court :
|
|
625
625
|
`read`/`start`/`write`/`open`/`close`/`destroy`/`gc`.
|
|
626
|
-
2. Ajouter les capacités **optionnelles** utiles : `touch` (idle glissant sans réécriture, `ISession.ts
|
|
627
|
-
`listPage` + `countSessions` (administration paginée, `ISession.ts
|
|
626
|
+
2. Ajouter les capacités **optionnelles** utiles : `touch` (idle glissant sans réécriture, `ISession.ts#ISessionStorage.touch`),
|
|
627
|
+
`listPage` + `countSessions` (administration paginée, `ISession.ts#ISessionStorage.countSessions`), `listAll` (dump).
|
|
628
628
|
3. Appeler `SessionsService.registerStorage("mon-store", MonStore)` au chargement du module.
|
|
629
629
|
4. Exécuter les bancs de contrat partagés (`sessionStoreContract.ts`, `sessionPaginationContract.ts`)
|
|
630
630
|
contre l'implémentation — c'est ce qui garantit la parité.
|
|
@@ -632,41 +632,41 @@ Pour ajouter un backend :
|
|
|
632
632
|
Trois règles de conception se dégagent du contrat, et méritent d'être respectées :
|
|
633
633
|
|
|
634
634
|
- **Une capacité absente s'annonce.** Ne pas implémenter `listPage` fait répondre **501** à l'endpoint
|
|
635
|
-
d'administration (refus honnête) plutôt qu'une liste vide trompeuse (`ISession.ts
|
|
635
|
+
d'administration (refus honnête) plutôt qu'une liste vide trompeuse (`ISession.ts#ISessionStorage.listPage`).
|
|
636
636
|
- **On n'invente pas ce qu'on ignore.** `countSessions()` renvoie `-1` quand compter coûterait trop
|
|
637
|
-
cher — Redis le fait (`ISession.ts
|
|
637
|
+
cher — Redis le fait (`ISession.ts#ISessionStorage.countSessions`).
|
|
638
638
|
- **Une page ne matérialise jamais plus qu'une page.** C'est ce qui rend le coût d'une requête
|
|
639
639
|
d'administration indépendant du nombre de sessions.
|
|
640
640
|
|
|
641
641
|
## 📜 Normes appliquées
|
|
642
642
|
|
|
643
|
-
| Domaine | Norme | Comment le code s'y conforme
|
|
644
|
-
| ----------------------------- | ------------------------- |
|
|
645
|
-
| Attributs et préfixes cookie | RFC 6265bis §4.1.3 | `__Host-` impose `Secure` + `Path=/`, interdit `Domain` (`cookie.ts
|
|
646
|
-
| Nom du cookie selon transport | RFC 6265bis / OWASP | `getSessionCookieName()` (`Context.ts
|
|
647
|
-
| Idle timeout | NIST SP 800-63B-4 / OWASP | défaut 1800 s, glissant par `touch` (`config.ts
|
|
648
|
-
| Absolute timeout | NIST SP 800-63B-4 / OWASP | défaut 43200 s, jamais prolongé (`config.ts
|
|
649
|
-
| Identifiant de session | OWASP Session Management | 32 octets CSPRNG, opaque (`session.ts
|
|
650
|
-
| Identifiant hors URL | OWASP Session Management | cookie uniquement — jamais de réécriture d'URL (`session.ts
|
|
651
|
-
| Renouvellement après auth | OWASP (anti-fixation) | `regenerateId()` inconditionnel au login (`authFlow.ts
|
|
652
|
-
| Révocation côté serveur | OWASP | pierre tombale générique (`RevocationGuardStorage.ts
|
|
643
|
+
| Domaine | Norme | Comment le code s'y conforme |
|
|
644
|
+
| ----------------------------- | ------------------------- | ----------------------------------------------------------------------------------------- |
|
|
645
|
+
| Attributs et préfixes cookie | RFC 6265bis §4.1.3 | `__Host-` impose `Secure` + `Path=/`, interdit `Domain` (`cookie.ts#Cookie.domain`) |
|
|
646
|
+
| Nom du cookie selon transport | RFC 6265bis / OWASP | `getSessionCookieName()` (`Context.ts#Context.getSessionCookieName`) |
|
|
647
|
+
| Idle timeout | NIST SP 800-63B-4 / OWASP | défaut 1800 s, glissant par `touch` (`config.ts#sessionSchema`) |
|
|
648
|
+
| Absolute timeout | NIST SP 800-63B-4 / OWASP | défaut 43200 s, jamais prolongé (`config.ts#sessionSchema`) |
|
|
649
|
+
| Identifiant de session | OWASP Session Management | 32 octets CSPRNG, opaque (`session.ts#Session.create`) |
|
|
650
|
+
| Identifiant hors URL | OWASP Session Management | cookie uniquement — jamais de réécriture d'URL (`session.ts#OptionsSessionType`) |
|
|
651
|
+
| Renouvellement après auth | OWASP (anti-fixation) | `regenerateId()` inconditionnel au login (`authFlow.ts#AuthFlow.#openSession`) |
|
|
652
|
+
| Révocation côté serveur | OWASP | pierre tombale générique (`RevocationGuardStorage.ts#RevocationGuardStorage.constructor`) |
|
|
653
653
|
|
|
654
654
|
## ⚡ Performance & mémoire
|
|
655
655
|
|
|
656
656
|
Le coût d'une session est **payé seulement quand elle sert** :
|
|
657
657
|
|
|
658
658
|
- **Zéro par défaut** — sans intent ni cookie, `startSession()` sort immédiatement
|
|
659
|
-
(`http-kernel.ts
|
|
660
|
-
- **Objet léger** — trois sacs `{}` à plat, pas de container DI par session (`session.ts
|
|
661
|
-
- **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts
|
|
662
|
-
`touch` est throttlé à une écriture par demi-vie d'idle (`session.ts
|
|
659
|
+
(`http-kernel.ts#HttpKernel.startSession`) : ni objet `Session`, ni lecture de store.
|
|
660
|
+
- **Objet léger** — trois sacs `{}` à plat, pas de container DI par session (`session.ts#Session`).
|
|
661
|
+
- **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts#Session.save`) ; le
|
|
662
|
+
`touch` est throttlé à une écriture par demi-vie d'idle (`session.ts#Session.touchIfNeeded`).
|
|
663
663
|
- **GC hors requête** — timer déterministe avec jitter par process, à la place du tirage
|
|
664
|
-
probabiliste hérité de PHP (`sessions-service.ts
|
|
664
|
+
probabiliste hérité de PHP (`sessions-service.ts#SessionsService.initializeStorage`).
|
|
665
665
|
- **Révocation quasi gratuite** — la `Map` de pierres tombales est **paresseuse** : sans révocation,
|
|
666
666
|
`write` ne paie qu'une comparaison `=== null`, sans même un `Date.now()`
|
|
667
|
-
(`RevocationGuardStorage.ts
|
|
667
|
+
(`RevocationGuardStorage.ts#RevocationGuardStorage.#isRevoked`).
|
|
668
668
|
- **Administration bornée** — jamais plus de `SCAN_PAGE = 200` enregistrements en mémoire
|
|
669
|
-
(`sessions-service.ts
|
|
669
|
+
(`sessions-service.ts#SCAN_PAGE`), garde-fou à 5 000 pages (`sessions-service.ts#MAX_ADMIN_PAGES`), parcours interrompu
|
|
670
670
|
**journalisé**.
|
|
671
671
|
|
|
672
672
|
Le banc `session-load.test.ts` verrouille ces propriétés sur serveur réel (200 sessions HTTP, 100
|
|
@@ -676,25 +676,25 @@ plus un plafond de croissance du tas.
|
|
|
676
676
|
> [!NOTE]
|
|
677
677
|
> Les trois sacs sont des objets **littéraux** et non `Object.create(null)`. C'est délibéré :
|
|
678
678
|
> `drizzle-orm` déréférence le prototype via `is()`, et un objet sans prototype ferait échouer
|
|
679
|
-
> l'écriture (`session.ts
|
|
679
|
+
> l'écriture (`session.ts#Session.attributesBag`).
|
|
680
680
|
|
|
681
681
|
## 📡 Observabilité — Studio
|
|
682
682
|
|
|
683
|
-
**Data plane** — `createHttpAdminApi()` (`HttpAdminApi.ts
|
|
683
|
+
**Data plane** — `createHttpAdminApi()` (`HttpAdminApi.ts#createHttpAdminApi`) expose la surface d'administration sous
|
|
684
684
|
`/nodefony/http/api/` :
|
|
685
685
|
|
|
686
|
-
| Route | Verbe | Accès | Rôle
|
|
687
|
-
| ----------------------------------- | ----- | ----------------------- |
|
|
688
|
-
| `sessions` | GET | — | état du sous-système (`HttpAdminApi.ts
|
|
689
|
-
| `sessions/list` | GET | `ROLE_NODEFONY_ADMIN` | page de sessions redactées (`HttpAdminApi.ts
|
|
690
|
-
| `sessions/{ref}/revoke` | POST | `ROLE_NODEFONY_ADMIN` | révoquer une session (`HttpAdminApi.ts
|
|
691
|
-
| `sessions/revoke-user/{identifier}` | POST | `ROLE_NODEFONY_ADMIN` | déconnecter partout (`HttpAdminApi.ts
|
|
692
|
-
| `sessions/mine` | GET | utilisateur authentifié | « mes appareils » (`HttpAdminApi.ts
|
|
693
|
-
| `sessions/mine/{ref}/revoke` | POST | utilisateur authentifié | fermer une de mes sessions (`HttpAdminApi.ts
|
|
686
|
+
| Route | Verbe | Accès | Rôle |
|
|
687
|
+
| ----------------------------------- | ----- | ----------------------- | ----------------------------------------------------------------- |
|
|
688
|
+
| `sessions` | GET | — | état du sous-système (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
689
|
+
| `sessions/list` | GET | `ROLE_NODEFONY_ADMIN` | page de sessions redactées (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
690
|
+
| `sessions/{ref}/revoke` | POST | `ROLE_NODEFONY_ADMIN` | révoquer une session (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
691
|
+
| `sessions/revoke-user/{identifier}` | POST | `ROLE_NODEFONY_ADMIN` | déconnecter partout (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
692
|
+
| `sessions/mine` | GET | utilisateur authentifié | « mes appareils » (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
693
|
+
| `sessions/mine/{ref}/revoke` | POST | utilisateur authentifié | fermer une de mes sessions (`HttpAdminApi.ts#createHttpAdminApi`) |
|
|
694
694
|
|
|
695
695
|
Les deux routes `mine` ne demandent pas de rôle, mais **ne sont pas anonymes** : la zone firewall des
|
|
696
696
|
API d'administration n'accepte que l'authenticator `session` (pas d'`anonymous`), et le périmètre est
|
|
697
|
-
pris sur l'identité ALS, jamais sur un paramètre client (`HttpAdminApi.ts
|
|
697
|
+
pris sur l'identité ALS, jamais sur un paramètre client (`HttpAdminApi.ts#createHttpAdminApi`).
|
|
698
698
|
|
|
699
699
|
Chaque ligne rendue porte **`current`** — vrai pour LA session qui a émis la requête, et pour elle
|
|
700
700
|
seule. C'est le « cet appareil » des consoles d'appareils connectés, et le client ne peut pas le
|
|
@@ -705,31 +705,31 @@ dérivation est faite une fois par page (`sessions-service.ts` `currentSessionRe
|
|
|
705
705
|
requête ne porte pas de session (appel interne, invocation CLI).
|
|
706
706
|
|
|
707
707
|
Codes de réponse à connaître : **501** si le store courant ne sait pas s'énumérer
|
|
708
|
-
(`supportsEnumeration()` faux — `HttpAdminApi.ts
|
|
708
|
+
(`supportsEnumeration()` faux — `HttpAdminApi.ts#SessionsAdmin.supportsEnumeration`), **503** si le service de session est absent, **404** pour une `ref` inconnue ou
|
|
709
709
|
une révocation sans effet, **401** sur `mine` sans identité.
|
|
710
710
|
|
|
711
711
|
**Écrans** — la page **Sessions** (`/nodefony/sessions`) de Studio liste les sessions vivantes par `ref` et permet la
|
|
712
712
|
révocation unitaire ou en masse (`@nodefony/studio/frontend/src/routes/sessions/`). L'écran **Stores**
|
|
713
713
|
affiche le backend réellement résolu, sa provenance et son emplacement physique : ces informations sont
|
|
714
|
-
publiées au boot par `registerStoreResolution()` (`sessions-service.ts
|
|
714
|
+
publiées au boot par `registerStoreResolution()` (`sessions-service.ts#SessionsService.initializeStorage`), avec le chemin du fichier
|
|
715
715
|
SQLite quand c'est pertinent (`SessionStorage.location`,
|
|
716
|
-
`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
716
|
+
`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.location`).
|
|
717
717
|
|
|
718
718
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
719
719
|
|
|
720
|
-
| Symptôme | Cause | Correction
|
|
721
|
-
| ------------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
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
|
|
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
|
|
724
|
-
| Mutation ignorée, WARNING « READONLY SESSION mutated » | La route est en `@UseSession({ readOnly: true })` | Retirer `readOnly` sur les routes qui écrivent (`session.ts
|
|
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
|
|
726
|
-
| Sessions perdues à chaque redémarrage ou entre pods | Store `memory` (volatil, per-pod) | Déclarer une infra (`NF_DATABASE_URL`/`NF_REDIS_URL`) ou nommer le store
|
|
727
|
-
| Le boot s'arrête sur « session store … inconnu » | Nom de store explicite non enregistré, en production | Charger le module fournisseur, ou corriger le nom (`sessions-service.ts
|
|
728
|
-
| Total des sessions affiché « inconnu » en admin | Store Redis : compter coûterait un `SCAN` complet | Comportement voulu — `countSessions()` rend `-1`, on n'invente pas
|
|
729
|
-
| Liste admin en 501 | Le store n'implémente pas `listPage` | Refus honnête ; utiliser un store énumérable pour l'administration
|
|
730
|
-
| Cookie sans `__Host-` en développement | Transport en clair : le navigateur rejetterait le préfixe | Normal en `http://` ; forcer avec `cookie.hostPrefix: true` derrière un proxy TLS
|
|
731
|
-
| Session qui n'expire jamais malgré l'inactivité | `idleTimeoutS: 0` (et/ou `absoluteTimeoutS: 0`) | Garder les défauts NIST ; l'absolute borne l'âge même sous activité
|
|
732
|
-
| 500 pendant l'arrêt du serveur, requête en vol | L'ORM se déconnecte avant le drain des serveurs | Dégradé gracieusement : le repository rend `null` (`@nodefony/drizzle/nodefony/src/SessionStorage.ts
|
|
720
|
+
| Symptôme | Cause | Correction |
|
|
721
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
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#HttpKernel.publishRuntimePorts`) |
|
|
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 |
|
|
724
|
+
| Mutation ignorée, WARNING « READONLY SESSION mutated » | La route est en `@UseSession({ readOnly: true })` | Retirer `readOnly` sur les routes qui écrivent (`session.ts#Session.readOnly`) |
|
|
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#RevocationGuardStorage.constructor`) |
|
|
726
|
+
| Sessions perdues à chaque redémarrage ou entre pods | Store `memory` (volatil, per-pod) | Déclarer une infra (`NF_DATABASE_URL`/`NF_REDIS_URL`) ou nommer le store |
|
|
727
|
+
| Le boot s'arrête sur « session store … inconnu » | Nom de store explicite non enregistré, en production | Charger le module fournisseur, ou corriger le nom (`sessions-service.ts#SessionsService`) |
|
|
728
|
+
| Total des sessions affiché « inconnu » en admin | Store Redis : compter coûterait un `SCAN` complet | Comportement voulu — `countSessions()` rend `-1`, on n'invente pas |
|
|
729
|
+
| Liste admin en 501 | Le store n'implémente pas `listPage` | Refus honnête ; utiliser un store énumérable pour l'administration |
|
|
730
|
+
| Cookie sans `__Host-` en développement | Transport en clair : le navigateur rejetterait le préfixe | Normal en `http://` ; forcer avec `cookie.hostPrefix: true` derrière un proxy TLS |
|
|
731
|
+
| Session qui n'expire jamais malgré l'inactivité | `idleTimeoutS: 0` (et/ou `absoluteTimeoutS: 0`) | Garder les défauts NIST ; l'absolute borne l'âge même sous activité |
|
|
732
|
+
| 500 pendant l'arrêt du serveur, requête en vol | L'ORM se déconnecte avant le drain des serveurs | Dégradé gracieusement : le repository rend `null` (`@nodefony/drizzle/nodefony/src/SessionStorage.ts#SessionStorage.connector`) |
|
|
733
733
|
|
|
734
734
|
## 🧪 Tests & couverture
|
|
735
735
|
|