@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- 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 +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- 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 +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- 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 +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- 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 +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- 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 +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- 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 +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- 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 +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- 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 +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -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 +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -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 +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/cookies.md
CHANGED
|
@@ -112,20 +112,20 @@ Nodefony ne se contente pas de proposer ces attributs : il **choisit des défaut
|
|
|
112
112
|
les invariants** que le navigateur exigerait de toute façon — pour que l'erreur ne parte pas sur le fil.
|
|
113
113
|
|
|
114
114
|
**Le défaut est fermé.** Un cookie créé sans options est `Secure` + `HttpOnly` + `SameSite=Lax`
|
|
115
|
-
(`cookieDefaultSettings`, `cookie.ts
|
|
115
|
+
(`cookieDefaultSettings`, `cookie.ts#cookieDefaultSettings`). Il faut **choisir** d'ouvrir (ex. `httpOnly: false` pour un
|
|
116
116
|
cookie lu en JS), jamais choisir de fermer.
|
|
117
117
|
|
|
118
118
|
**Les préfixes sont forcés, pas espérés.** Nommer un cookie `__Host-…` ne suffit pas : `serialize()`
|
|
119
|
-
(`cookie.ts
|
|
119
|
+
(`cookie.ts#Cookie.serialize`) **réécrit** la sortie — `Secure` ajouté, `Path=/` imposé, `Domain` retiré — pour que le
|
|
120
120
|
navigateur ne rejette jamais le cookie en silence. Idem `__Secure-` (Secure imposé) et `SameSite=None` (qui
|
|
121
|
-
impose `Secure`, `cookie.ts
|
|
121
|
+
impose `Secure`, `cookie.ts#Cookie.secure`).
|
|
122
122
|
|
|
123
123
|
**La signature refuse le secret prévisible.** Un cookie `signed: true` sans secret configuré **jette**
|
|
124
|
-
(`setValue()`, `cookie.ts
|
|
125
|
-
(fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts
|
|
124
|
+
(`setValue()`, `cookie.ts#Cookie.setValue`) : signer avec le secret public par défaut ne protégerait rien
|
|
125
|
+
(fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts#Cookie.unsign`).
|
|
126
126
|
|
|
127
127
|
**`SameSite` retombe toujours sur `Lax`, jamais sur `None`.** Toute valeur inconnue est normalisée vers
|
|
128
|
-
`Lax` (`setSameSite()`, `cookie.ts
|
|
128
|
+
`Lax` (`setSameSite()`, `cookie.ts#Cookie.setSameSite`) : `None` désactive la protection CSRF, ce n'est jamais un défaut.
|
|
129
129
|
|
|
130
130
|
**HTTP et WebSocket lisent les mêmes cookies.** Le parseur tourne pour les deux transports ; en revanche la
|
|
131
131
|
poignée de main WebSocket **ne peut pas** écrire de cookie (limite de la bibliothèque `ws`) — voir plus bas.
|
|
@@ -201,10 +201,13 @@ Pour qu'une valeur ne puisse pas être **falsifiée** côté client (sans la chi
|
|
|
201
201
|
secret **configuré** (le secret par défaut est refusé) :
|
|
202
202
|
|
|
203
203
|
```typescript ignore
|
|
204
|
-
// fragment — un secret prévisible est refusé (fail-closed)
|
|
204
|
+
// fragment — un secret prévisible est refusé (fail-closed).
|
|
205
|
+
// `NF_COOKIE_SECRET` se déclare dans `env.ts` (`envString`), seul lecteur de l'environnement.
|
|
206
|
+
import { env } from "../../env";
|
|
207
|
+
|
|
205
208
|
const c = new Cookie("pref", "v1", {
|
|
206
209
|
signed: true,
|
|
207
|
-
secret:
|
|
210
|
+
secret: env.NF_COOKIE_SECRET,
|
|
208
211
|
});
|
|
209
212
|
// c.value === "s:v1.<hmac base64url>" ; c.unsign() renvoie "v1" ou false si altéré
|
|
210
213
|
```
|
|
@@ -218,120 +221,124 @@ suit montre l'**usage réel**. La classe s'importe `import { Cookie } from "@nod
|
|
|
218
221
|
### Construire un cookie et ses attributs
|
|
219
222
|
|
|
220
223
|
Le constructeur accepte `(nom, valeur, options?)` ou **un cookie à copier** (surcharge,
|
|
221
|
-
`cookie.ts
|
|
222
|
-
|
|
223
|
-
| Option | Type | Défaut | Effet
|
|
224
|
-
| ---------- | -------------------------- | ------------ |
|
|
225
|
-
| `maxAge` | `number` (secondes) | `0` | Durée de vie. `0` = cookie de session (ni `Max-Age` ni `Expires`).
|
|
226
|
-
| `expires` | `Date \| string \| number` | — | Date d'expiration absolue (`setExpires()`, `cookie.ts
|
|
227
|
-
| `path` | `string` | `/` | Préfixe d'URL de portée.
|
|
228
|
-
| `domain` | `string` | `undefined` | Domaine de portée (retiré si nom `__Host-`).
|
|
229
|
-
| `secure` | `boolean` | `true` | HTTPS only. Forcé si `SameSite=None` ou préfixe `__Host-`/`__Secure-`.
|
|
230
|
-
| `httpOnly` | `boolean` | `true` | Invisible à `document.cookie` (anti-XSS).
|
|
231
|
-
| `sameSite` | `SameSiteType` | `Lax` | `Strict`/`Lax`/`None`. Toute autre valeur retombe sur `Lax`.
|
|
232
|
-
| `signed` | `boolean` | `false` | Signe la valeur (HMAC). Exige `secret` configuré, sinon **jette**.
|
|
233
|
-
| `secret` | `string` | (par défaut) | Clé HMAC. Le secret par défaut est **refusé** pour signer.
|
|
234
|
-
| `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts
|
|
235
|
-
|
|
236
|
-
Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts
|
|
237
|
-
constructeur de `Cookie` (`cookie.ts
|
|
224
|
+
`cookie.ts#Cookie.constructor`). Les options fusionnent avec les défauts sûrs.
|
|
225
|
+
|
|
226
|
+
| Option | Type | Défaut | Effet |
|
|
227
|
+
| ---------- | -------------------------- | ------------ | -------------------------------------------------------------------------- |
|
|
228
|
+
| `maxAge` | `number` (secondes) | `0` | Durée de vie. `0` = cookie de session (ni `Max-Age` ni `Expires`). |
|
|
229
|
+
| `expires` | `Date \| string \| number` | — | Date d'expiration absolue (`setExpires()`, `cookie.ts#Cookie.setExpires`). |
|
|
230
|
+
| `path` | `string` | `/` | Préfixe d'URL de portée. |
|
|
231
|
+
| `domain` | `string` | `undefined` | Domaine de portée (retiré si nom `__Host-`). |
|
|
232
|
+
| `secure` | `boolean` | `true` | HTTPS only. Forcé si `SameSite=None` ou préfixe `__Host-`/`__Secure-`. |
|
|
233
|
+
| `httpOnly` | `boolean` | `true` | Invisible à `document.cookie` (anti-XSS). |
|
|
234
|
+
| `sameSite` | `SameSiteType` | `Lax` | `Strict`/`Lax`/`None`. Toute autre valeur retombe sur `Lax`. |
|
|
235
|
+
| `signed` | `boolean` | `false` | Signe la valeur (HMAC). Exige `secret` configuré, sinon **jette**. |
|
|
236
|
+
| `secret` | `string` | (par défaut) | Clé HMAC. Le secret par défaut est **refusé** pour signer. |
|
|
237
|
+
| `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts#Cookie.setPriority`). |
|
|
238
|
+
|
|
239
|
+
Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts#cookieDefaultSettings`) ; la fusion se fait dans le
|
|
240
|
+
constructeur de `Cookie` (`cookie.ts#Cookie.options`).
|
|
238
241
|
|
|
239
242
|
### Sérialiser : `serialize()` et `serializeWebSocket()`
|
|
240
243
|
|
|
241
|
-
- `serialize()` (`cookie.ts
|
|
244
|
+
- `serialize()` (`cookie.ts#Cookie.serialize`) produit la **ligne `Set-Cookie`** complète (attributs dans l'ordre,
|
|
242
245
|
préfixes forcés, `Max-Age` seulement s'il est positif).
|
|
243
|
-
- `serializeWebSocket()` (`cookie.ts
|
|
246
|
+
- `serializeWebSocket()` (`cookie.ts#Cookie.serializeWebSocket`) produit un **objet** `IWsCookie` (mêmes invariants de sécurité)
|
|
244
247
|
— utilisé quand un cookie doit être décrit hors en-tête HTTP.
|
|
245
|
-
- `toString()` (`cookie.ts
|
|
248
|
+
- `toString()` (`cookie.ts#Cookie.toString`) ne rend que `nom=valeurEncodée` (sans attributs).
|
|
246
249
|
|
|
247
250
|
### Signer / vérifier : `sign()` et `unsign()`
|
|
248
251
|
|
|
249
|
-
- `sign(val, secret)` (`cookie.ts
|
|
252
|
+
- `sign(val, secret)` (`cookie.ts#Cookie.sign`) → `val.base64url(HMAC-SHA256)` — la valeur d'origine est
|
|
250
253
|
**préservée** (récupérable), la signature garantit l'intégrité.
|
|
251
|
-
- `unsign(val?, secret?)` (`cookie.ts
|
|
252
|
-
Comparaison **timing-safe** (`cookie.ts
|
|
254
|
+
- `unsign(val?, secret?)` (`cookie.ts#Cookie.unsign`) → la valeur en clair si la signature est valide, sinon `false`.
|
|
255
|
+
Comparaison **timing-safe** (`cookie.ts#Cookie.unsign`) ; tolère le préfixe marqueur `s:`.
|
|
253
256
|
|
|
254
257
|
### Lire et écrire depuis le contexte
|
|
255
258
|
|
|
256
|
-
| Besoin | Appel | Où
|
|
257
|
-
| ------------------------------ | ----------------------------------------------------- |
|
|
258
|
-
| Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts
|
|
259
|
-
| Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts
|
|
260
|
-
| Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts
|
|
261
|
-
| Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts
|
|
259
|
+
| Besoin | Appel | Où |
|
|
260
|
+
| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------- |
|
|
261
|
+
| Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts#Context.getRequestCookies`) |
|
|
262
|
+
| Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts#Context.cookies`) |
|
|
263
|
+
| Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts#Context.setCookie`) |
|
|
264
|
+
| Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts#HttpResponse.deleteCookieByName` |
|
|
262
265
|
|
|
263
|
-
Côté réponse HTTP, `addCookie()` (`http/Response.ts
|
|
264
|
-
(`http/Response.ts
|
|
266
|
+
Côté réponse HTTP, `addCookie()` (`http/Response.ts#HttpResponse.addCookie`) enregistre le cookie, et `setCookies()`
|
|
267
|
+
(`http/Response.ts#HttpResponse.setCookies`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
|
|
265
268
|
boucle de `setHeader` (qui écraserait tout sauf le dernier). Pour expirer un cookie chez le client :
|
|
266
|
-
`clearCookie()` (`cookie.ts
|
|
269
|
+
`clearCookie()` (`cookie.ts#Cookie.clearCookie`) recule `Expires` à l'époque.
|
|
267
270
|
|
|
268
271
|
### Parsing des cookies entrants
|
|
269
272
|
|
|
270
|
-
`cookiesParser(context)` (`cookie.ts
|
|
271
|
-
`parser()` `cookie.ts
|
|
272
|
-
(`Context.ts
|
|
273
|
-
l'initialisation du contexte HTTP (`HttpContext.ts
|
|
273
|
+
`cookiesParser(context)` (`cookie.ts#cookiesParser`) lit l'en-tête `Cookie:` (via la bibliothèque `cookie`,
|
|
274
|
+
`parser()` `cookie.ts#parser`), crée un `Cookie` par entrée et l'ajoute au contexte avec `addRequestCookie()`
|
|
275
|
+
(`Context.ts#Context.addRequestCookie`). Il est déclenché automatiquement par le pipeline : `parseCookies()` est appelé à
|
|
276
|
+
l'initialisation du contexte HTTP (`HttpContext.ts#HttpContext.constructor`) **et** WebSocket (`WebsocketContext.ts#WebsocketContext.constructor`).
|
|
274
277
|
|
|
275
278
|
### Côté WebSocket — lecture oui, écriture non
|
|
276
279
|
|
|
277
280
|
Les cookies **entrants** sont lus au handshake, exactement comme en HTTP (même parseur). Mais la **poignée
|
|
278
281
|
de main WebSocket ne peut pas poser de cookie** : `setCookie()` et `setCookies()` de la réponse WS sont des
|
|
279
|
-
**no-op** (`websocket/Response.ts
|
|
282
|
+
**no-op** (`websocket/Response.ts#WebsocketResponse.setCookies`), une limite de la bibliothèque `ws`. Le cookie de session, lui, est
|
|
280
283
|
posé pendant la **phase HTTP** qui précède l'upgrade. La forme d'un cookie décrit pour le WS est
|
|
281
|
-
`IWsCookie` (`ICookie.ts
|
|
284
|
+
`IWsCookie` (`ICookie.ts#IWsCookie`), produite par `serializeWebSocket()`.
|
|
282
285
|
|
|
283
286
|
## ⚙️ Configuration
|
|
284
287
|
|
|
285
288
|
Les cookies **applicatifs** ne se configurent pas par schéma : on les construit dans le code, avec les
|
|
286
|
-
défauts sûrs de `cookieDefaultSettings` (`cookie.ts
|
|
287
|
-
de la **session** — bloc Zod `sessionCookieSchema` (`config.ts
|
|
288
|
-
(`config.ts
|
|
289
|
+
défauts sûrs de `cookieDefaultSettings` (`cookie.ts#cookieDefaultSettings`). Le seul cookie **piloté par la config** est celui
|
|
290
|
+
de la **session** — bloc Zod `sessionCookieSchema` (`config.ts#sessionCookieSchema`), avec notamment `hostPrefix`
|
|
291
|
+
(`config.ts#sessionCookieSchema`) qui décide du préfixe `__Host-`. Tout cela est documenté dans [Sessions](session.md) :
|
|
289
292
|
cette page ne le duplique pas.
|
|
290
293
|
|
|
291
294
|
Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport) est calculé par
|
|
292
|
-
`getSessionCookieName()` (`Context.ts
|
|
295
|
+
`getSessionCookieName()` (`Context.ts#Context.getSessionCookieName`) — encore un détail qui appartient à la page Sessions.
|
|
293
296
|
|
|
294
297
|
## 🛡️ Défenses par attribut
|
|
295
298
|
|
|
296
|
-
| Attribut / mécanisme | Faille bloquée | Comment Nodefony l'applique
|
|
297
|
-
| -------------------------- | --------------------------------------- |
|
|
298
|
-
| `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts
|
|
299
|
-
| `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts
|
|
300
|
-
| `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts
|
|
301
|
-
| Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts
|
|
302
|
-
| Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts
|
|
303
|
-
| Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts
|
|
299
|
+
| Attribut / mécanisme | Faille bloquée | Comment Nodefony l'applique |
|
|
300
|
+
| -------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
301
|
+
| `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts#cookieDefaultSettings`) |
|
|
302
|
+
| `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts#Cookie.setSameSite`) |
|
|
303
|
+
| `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts#Cookie.serialize`) |
|
|
304
|
+
| Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts#Cookie.serialize`) |
|
|
305
|
+
| Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts#Cookie.unsign`) |
|
|
306
|
+
| Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts#Cookie.setValue`) |
|
|
304
307
|
|
|
305
308
|
## 📜 Normes appliquées
|
|
306
309
|
|
|
307
|
-
| Domaine | Norme | Ancrage
|
|
308
|
-
| ---------------------------------- | -------------------- |
|
|
309
|
-
| Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts
|
|
310
|
-
| `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts
|
|
311
|
-
| Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts
|
|
312
|
-
| `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts
|
|
313
|
-
| Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts
|
|
314
|
-
| Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts
|
|
310
|
+
| Domaine | Norme | Ancrage |
|
|
311
|
+
| ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
|
|
312
|
+
| Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts#Cookie.serialize`) |
|
|
313
|
+
| `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts#SameSiteType`), `setSameSite()` (`cookie.ts#Cookie.setSameSite`) |
|
|
314
|
+
| Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts#Cookie.serialize`) |
|
|
315
|
+
| `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts#Cookie.serialize`) |
|
|
316
|
+
| Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts#Cookie.sign`) |
|
|
317
|
+
| Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts#Cookie.unsign`) |
|
|
315
318
|
|
|
316
319
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
317
320
|
|
|
318
|
-
| Symptôme | Cause | Correction
|
|
319
|
-
| --------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
320
|
-
| Le cookie posé en HTTP clair n'est pas stocké | Défaut `Secure: true` → le navigateur exige HTTPS (sauf `localhost`) | Servir en HTTPS, ou `secure: false` en dev hors localhost
|
|
321
|
-
| Un cookie `__Host-` ignore mon `Domain`/`Path` | `serialize()` **force** les contraintes du préfixe | Attendu (RFC 6265bis) — renommer sans préfixe si tu veux un `Domain`
|
|
322
|
-
| `new Cookie(..., { signed: true })` **jette** | Aucun `secret` configuré → refus du secret prévisible (fail-closed) | Passer un `secret` réel (`{ signed: true, secret: … }`)
|
|
323
|
-
| Modifier un cookie entrant ne change rien côté client | Lecture (`context.cookies`) et écriture (réponse) ne sont pas symétriques | (Re)poser le cookie sur la réponse : `context.setCookie(new Cookie(…))`
|
|
324
|
-
| Le cookie WS posé au handshake n'arrive jamais | `setCookie`/`setCookies` de la réponse WS sont des no-op (`ws`) | Poser le cookie pendant la phase HTTP avant l'upgrade (cf session)
|
|
325
|
-
| Deux `Set-Cookie` s'écrasent, un seul survit | Un `setHeader('Set-Cookie', str)` remplace le précédent | Déjà géré : `setCookies()` passe un **tableau** (`http/Response.ts
|
|
326
|
-
| `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement
|
|
327
|
-
| `maxAge` interprété en millisecondes | `maxAge` est en **secondes** (comme `Set-Cookie` Max-Age) | Passer des secondes (`30*24*60*60`), pas des ms
|
|
321
|
+
| Symptôme | Cause | Correction |
|
|
322
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
323
|
+
| Le cookie posé en HTTP clair n'est pas stocké | Défaut `Secure: true` → le navigateur exige HTTPS (sauf `localhost`) | Servir en HTTPS, ou `secure: false` en dev hors localhost |
|
|
324
|
+
| Un cookie `__Host-` ignore mon `Domain`/`Path` | `serialize()` **force** les contraintes du préfixe | Attendu (RFC 6265bis) — renommer sans préfixe si tu veux un `Domain` |
|
|
325
|
+
| `new Cookie(..., { signed: true })` **jette** | Aucun `secret` configuré → refus du secret prévisible (fail-closed) | Passer un `secret` réel (`{ signed: true, secret: … }`) |
|
|
326
|
+
| Modifier un cookie entrant ne change rien côté client | Lecture (`context.cookies`) et écriture (réponse) ne sont pas symétriques | (Re)poser le cookie sur la réponse : `context.setCookie(new Cookie(…))` |
|
|
327
|
+
| Le cookie WS posé au handshake n'arrive jamais | `setCookie`/`setCookies` de la réponse WS sont des no-op (`ws`) | Poser le cookie pendant la phase HTTP avant l'upgrade (cf session) |
|
|
328
|
+
| Deux `Set-Cookie` s'écrasent, un seul survit | Un `setHeader('Set-Cookie', str)` remplace le précédent | Déjà géré : `setCookies()` passe un **tableau** (`http/Response.ts#HttpResponse.setCookies`) |
|
|
329
|
+
| `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement |
|
|
330
|
+
| `maxAge` interprété en millisecondes | `maxAge` est en **secondes** (comme `Set-Cookie` Max-Age) | Passer des secondes (`30*24*60*60`), pas des ms |
|
|
328
331
|
|
|
329
332
|
## 📡 Observabilité
|
|
330
333
|
|
|
331
334
|
Il n'y a pas d'écran Studio dédié aux cookies applicatifs (le cookie **de session** est surfacé dans
|
|
332
335
|
l'écran **Sessions**). En développement, chaque écriture de cookie est journalisée en `DEBUG` par la
|
|
333
|
-
réponse HTTP
|
|
334
|
-
`nodefony-tail-error-logs` ou le Suivi
|
|
336
|
+
réponse HTTP — nom et attributs, **valeur masquée** (`ADD COOKIE ==> nodefony=<masqué>; Path=/; …`,
|
|
337
|
+
`http/Response.ts#HttpResponse.setCookie`) — visible via le skill `nodefony-tail-error-logs` ou le Suivi
|
|
338
|
+
de requête. La valeur n'y figure jamais : ce journal se lit par le plan d'administration, et un
|
|
339
|
+
identifiant de session y passerait à quiconque le consulte, agent de code compris. La ligne n'est
|
|
340
|
+
construite que si `DEBUG` passe le seuil du journal (debug ciblé compris) : en production, rien n'est
|
|
341
|
+
alloué. La valeur se lit sur le fil : un `curl -i` montre les lignes `Set-Cookie`.
|
|
335
342
|
|
|
336
343
|
## 🧪 Tests & couverture
|
|
337
344
|
|
package/docs/index.md
CHANGED
|
@@ -66,6 +66,7 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
|
|
|
66
66
|
| [Upload & corps](upload.md) | parser le corps et recevoir des fichiers | formulaires, imports multipart, API JSON |
|
|
67
67
|
| [Rate-limit](rate-limit.md) | plafonner le débit par client (429) | protéger une API d'un flood ou d'un abus |
|
|
68
68
|
| [Observabilité](observabilite.md) | tracer et journaliser chaque requête | débugger en prod, corréler des logs |
|
|
69
|
+
| [Proxy inverse](reverse-proxy.md) | relayer un préfixe d'URL vers un autre serveur, sur la même origine | servir Vite en dev, adosser un service interne sans CORS |
|
|
69
70
|
| [Pipeline de requête](../../../../../docs/architecture/pipeline-requete.md) | l'ordre exact des étapes, HTTP comme WS | tu débugges « pourquoi ça passe / ça bloque ici » |
|
|
70
71
|
|
|
71
72
|
```nodefony-cards
|
|
@@ -88,6 +89,9 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
|
|
|
88
89
|
{ "icon": "📈", "title": "observabilite", "href": "observabilite.md",
|
|
89
90
|
"desc": "Observer les requêtes : lignes de log (pretty ou JSON), requestId de corrélation, W3C Trace Context, trace des frames WebSocket, redaction et sampling d'audit. Où partent les logs est traité par la page Syslog du cœur.",
|
|
90
91
|
"meta": "débugger en prod, corréler les logs par requête" },
|
|
92
|
+
{ "icon": "↪️", "title": "reverse-proxy", "href": "reverse-proxy.md",
|
|
93
|
+
"desc": "Relayer un préfixe d'URL vers un autre serveur sans quitter l'origine de la page : HTTP et WebSocket, en-têtes de connexion jamais transmis, X-Forwarded-* et Via posés, barrière d'hôte appliquée avant le relais. C'est par lui que Vite est servi en développement.",
|
|
94
|
+
"meta": "une origine, un certificat — pas de CORS ni de contenu mixte" },
|
|
91
95
|
{ "icon": "🔀", "title": "pipeline-requete", "href": "../../../../../docs/architecture/pipeline-requete.md",
|
|
92
96
|
"desc": "Où ce module s'arrête et où le framework prend le relais, et dans quel ordre s'enchaînent contexte, rate-limit, routage, session, CSRF et firewall.",
|
|
93
97
|
"meta": "page transverse — celle qui relie tout" }
|
|
@@ -134,7 +138,7 @@ de brique détaille son bloc et ses défauts réels.
|
|
|
134
138
|
## 📜 Normes appliquées
|
|
135
139
|
|
|
136
140
|
RFC 9110/9111/9112 (sémantique HTTP, cache, HTTP/1.1), RFC 9113 (HTTP/2), RFC 6455 (WebSocket et ses
|
|
137
|
-
codes de fermeture), RFC 6265bis (cookies), RFC 6585 (429), RFC
|
|
141
|
+
codes de fermeture), RFC 6265bis (cookies), RFC 6585 (429), RFC 9525 (identité des certificats),
|
|
138
142
|
WHATWG Fetch (CORS), W3C Trace Context (`traceparent`).
|
|
139
143
|
|
|
140
144
|
## 📡 Observabilité — Studio
|
package/docs/observabilite.md
CHANGED
|
@@ -75,7 +75,7 @@ Trois idées portent tout le reste :
|
|
|
75
75
|
| `X-Request-Id` | En-tête HTTP de **convention** (de-facto, non normalisé) portant le `requestId` : le client peut l'imposer, le serveur le réfléchit. |
|
|
76
76
|
| Corrélation | Relier des lignes de log éparses à la **même** requête via une clé partagée (`requestId`, `pid`). |
|
|
77
77
|
| ALS | _AsyncLocalStorage_ : mémoire Node attachée au contexte d'exécution async — propage `requestId` sans le passer en argument. |
|
|
78
|
-
| `RequestContext` | Façade Nodefony au-dessus de l'ALS : `getRequestId()`, `
|
|
78
|
+
| `RequestContext` | Façade Nodefony au-dessus de l'ALS : `getRequestId()`, `getAuthenticatedUser()`, `getUserId()`, `traceparent`. |
|
|
79
79
|
| `Pdu` | _Process Data Unit_ : une entrée de log structurée (RFC 5424). Porte `requestId` + `pid`. |
|
|
80
80
|
| Syslog | Le hub de logs du cœur (ring buffer + drivers). Reçoit chaque `Pdu` — **page distincte**. |
|
|
81
81
|
| `traceparent` | En-tête **W3C Trace Context** : `version-traceId-spanId-flags`. Corrèle une requête à travers plusieurs services. |
|
|
@@ -111,19 +111,19 @@ Le différenciateur — **HTTP et WebSocket dans le même pipeline** — se retr
|
|
|
111
111
|
un `requestId`, un `traceparent` et un contrat de logger **uniques** couvrent les deux transports.
|
|
112
112
|
|
|
113
113
|
**Le `requestId` est un citoyen du contexte, pas un décor.** Il naît dans le constructeur de base
|
|
114
|
-
`Context.requestId = randomUUID()` (`Context.ts
|
|
115
|
-
(`http-kernel.ts
|
|
114
|
+
`Context.requestId = randomUUID()` (`Context.ts#Context.csrfProtect`), voyage dans l'ALS via `RequestContext.run(...)`
|
|
115
|
+
(`http-kernel.ts#HttpKernel.configureRateLimit` pour HTTP, `http-kernel.ts#HttpKernel.finishWebsocket` pour WS), et se lit de n'importe où avec
|
|
116
116
|
`RequestContext.getRequestId()` — un controller, un service, un adapter ORM, sans jamais le threader.
|
|
117
117
|
|
|
118
118
|
**La ligne de bilan est branchable.** Le kernel ne code pas un format en dur : il consulte un
|
|
119
|
-
`IRequestLogger` (`IRequestLogger.ts
|
|
120
|
-
`http-kernel.ts
|
|
119
|
+
`IRequestLogger` (`IRequestLogger.ts#IRequestLogger`) résolu au boot depuis la config (`applyRequestLoggerFromConfig`,
|
|
120
|
+
`http-kernel.ts#HttpKernel.applyRequestLoggerFromConfig`), remplaçable à chaud par `httpKernel.setRequestLogger(...)` (`http-kernel.ts#HttpKernel.setRequestLogger`).
|
|
121
121
|
Trois formateurs sont livrés ; un quatrième maison s'écrit en implémentant l'interface.
|
|
122
122
|
|
|
123
123
|
**Zero Trust sur l'entrée cliente.** Un `X-Request-Id` fourni par le client finit réfléchi en réponse,
|
|
124
124
|
écrit dans les logs et propagé en ALS — donc une valeur non assainie ouvrirait log-injection (CR/LF) et
|
|
125
125
|
throw natif de `setHeader`. Nodefony **valide puis adopte, ou rejette** (`sanitizeRequestId`,
|
|
126
|
-
`requestId.ts
|
|
126
|
+
`requestId.ts#sanitizeRequestId`) : jamais nettoyer (masquerait l'abus), toujours retomber sur l'UUID serveur.
|
|
127
127
|
|
|
128
128
|
## 🚀 Démarrage rapide
|
|
129
129
|
|
|
@@ -222,43 +222,43 @@ GET 200 /trace/whoami 3.1ms 127.0.0.1 [demo-abc]
|
|
|
222
222
|
|
|
223
223
|
### Le `requestId` — génération, adoption, réflexion
|
|
224
224
|
|
|
225
|
-
| Étape | Où
|
|
226
|
-
| ------------------- |
|
|
227
|
-
| Génération | `Context.requestId = randomUUID()` (`Context.ts
|
|
228
|
-
| Adoption HTTP | `sanitizeRequestId(headers["x-request-id"])` (`HttpContext.ts
|
|
229
|
-
| Adoption WS | `sanitizeRequestId(...)` au handshake (`WebsocketContext.ts
|
|
230
|
-
| Réflexion HTTP/1.1 | `Response.setHeader("x-request-id", …)` (`Response.ts
|
|
231
|
-
| Réflexion HTTP/2 | `this.headers["x-request-id"] = requestId` (`http2/Response.ts
|
|
232
|
-
| ALS (HTTP) | `RequestContext.run({ requestId, … })` (`http-kernel.ts
|
|
233
|
-
| ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts
|
|
234
|
-
| Capture dans le log | `Pdu.requestId = Pdu.requestIdProvider?.()` (`Pdu.ts
|
|
225
|
+
| Étape | Où | Comportement |
|
|
226
|
+
| ------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| Génération | `Context.requestId = randomUUID()` (`Context.ts#Context.csrfProtect`) | UUID v4 posé dans le constructeur de base — HTTP **et** WS. |
|
|
228
|
+
| Adoption HTTP | `sanitizeRequestId(headers["x-request-id"])` (`HttpContext.ts#HttpContext.response`) | Remplace l'UUID **si** la valeur cliente est sûre, sinon on garde l'UUID. |
|
|
229
|
+
| Adoption WS | `sanitizeRequestId(...)` au handshake (`WebsocketContext.ts#WebsocketContext.constructor`) | Même validation, stable sur toute la durée de la socket (handshake → close). |
|
|
230
|
+
| Réflexion HTTP/1.1 | `Response.setHeader("x-request-id", …)` (`Response.ts#HttpResponse.setHeader`) | Écrit dans `writeHead()`, sur **chaque** réponse. |
|
|
231
|
+
| Réflexion HTTP/2 | `this.headers["x-request-id"] = requestId` (`http2/Response.ts#Http2Response.writeHead`) | Sinon les réponses du port 5152 sortiraient sans corrélation. |
|
|
232
|
+
| ALS (HTTP) | `RequestContext.run({ requestId, … })` (`http-kernel.ts#HttpKernel.configureRateLimit`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
|
|
233
|
+
| ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts#HttpKernel.configureRateLimit`) | Handshake **et** messages : la bulle ouverte à la connexion est reliée à chaque message par `AsyncResource.bind`, sinon l'identité résolue au handshake se perdrait au premier message. |
|
|
234
|
+
| Capture dans le log | `Pdu.requestId = Pdu.requestIdProvider?.()` (`Pdu.ts#Pdu.sysLogSeverity`) | Provider injectable branché sur l'ALS côté Node — 0 lecture côté navigateur. |
|
|
235
235
|
|
|
236
236
|
> [!IMPORTANT]
|
|
237
237
|
> Les logs de **fin** de requête (bilan `req`, `onClose`) sont émis **hors** de la bulle ALS (déjà
|
|
238
|
-
> refermée). L'override `Context.log()` (`Context.ts
|
|
238
|
+
> refermée). L'override `Context.log()` (`Context.ts#Context.log`) rouvre alors une micro-bulle depuis
|
|
239
239
|
> `this.requestId` pour que le `Pdu` capture quand même la corrélation — sinon la ligne d'entrée d'une
|
|
240
240
|
> trace serait la seule à ne PAS porter son `requestId`.
|
|
241
241
|
|
|
242
242
|
### La trace W3C — `traceparent`
|
|
243
243
|
|
|
244
|
-
Nodefony implémente **W3C Trace Context** (le code s'y réfère explicitement, `trace.ts
|
|
244
|
+
Nodefony implémente **W3C Trace Context** (le code s'y réfère explicitement, `trace.ts`). À l'entrée :
|
|
245
245
|
|
|
246
246
|
- Un `traceparent` entrant **valide** est prolongé : on garde `version`/`traceId`/`flags` et on frappe
|
|
247
|
-
un nouveau `spanId` (on est un maillon enfant) — `resolveTraceparent()` (`trace.ts
|
|
247
|
+
un nouveau `spanId` (on est un maillon enfant) — `resolveTraceparent()` (`trace.ts#resolveTraceparent`).
|
|
248
248
|
- Absent ou malformé → on **forge** une trace neuve (`00-<traceId>-<spanId>-01`, échantillonnée).
|
|
249
249
|
- La validation refuse `version=ff` et un `traceId`/`spanId` tout-à-zéro — `parseTraceparent()`
|
|
250
|
-
(`trace.ts
|
|
250
|
+
(`trace.ts#parseTraceparent`), conforme à la spec (le récepteur NE DOIT PAS propager ces valeurs).
|
|
251
251
|
|
|
252
|
-
Le `traceparent` résolu est propagé en ALS **et** réfléchi sur la réponse HTTP (`context/http/Response.ts
|
|
252
|
+
Le `traceparent` résolu est propagé en ALS **et** réfléchi sur la réponse HTTP (`context/http/Response.ts#HttpResponse.writeHead`). Côté
|
|
253
253
|
**WebSocket**, il est propagé en ALS mais **pas** réfléchi dans la réponse de handshake — la bibliothèque
|
|
254
|
-
`ws` n'expose pas proprement ce chemin (`http-kernel.ts
|
|
254
|
+
`ws` n'expose pas proprement ce chemin (`http-kernel.ts#HttpKernel.finishTeardownHttp`) ; la corrélation reste visible côté serveur.
|
|
255
255
|
|
|
256
256
|
### Le contrat de logger — `IRequestLogger`
|
|
257
257
|
|
|
258
|
-
Le kernel tient un `IRequestLogger` singleton (`http-kernel.ts
|
|
259
|
-
de bilan, au teardown, via `Context.logRequest()` (`Context.ts
|
|
260
|
-
`WebsocketContext.logRequest()` (`WebsocketContext.ts
|
|
261
|
-
(`IRequestLogger.ts
|
|
258
|
+
Le kernel tient un `IRequestLogger` singleton (`http-kernel.ts#HttpKernel.secContentTypeOptions`) et lui délègue le rendu de la ligne
|
|
259
|
+
de bilan, au teardown, via `Context.logRequest()` (`Context.ts#Context.logRequest`) côté HTTP et
|
|
260
|
+
`WebsocketContext.logRequest()` (`WebsocketContext.ts#WebsocketContext.logRequest`) côté WS. Le contrat a trois méthodes
|
|
261
|
+
(`IRequestLogger.ts#IRequestLogger`) :
|
|
262
262
|
|
|
263
263
|
- `renderHttp(context, error?)` → `{ text, severity, msgid }` remis à `context.log()`.
|
|
264
264
|
- `renderWebsocket(context, error?, acceptedProtocol?)` → idem pour le WS.
|
|
@@ -268,38 +268,38 @@ de bilan, au teardown, via `Context.logRequest()` (`Context.ts:608`) côté HTTP
|
|
|
268
268
|
### La trace des frames WebSocket
|
|
269
269
|
|
|
270
270
|
Chaque frame WS (RECEIVE / SEND / BROADCAST) peut être tracée pour le Suivi de requête (Studio). Le
|
|
271
|
-
formatage du contenu est **pur** et **borné** — `formatWsLogContent()` (`wsLogContent.ts
|
|
272
|
-
`WebsocketContext.logMessageContent()` (`WebsocketContext.ts
|
|
271
|
+
formatage du contenu est **pur** et **borné** — `formatWsLogContent()` (`wsLogContent.ts#formatWsLogContent`), appelé par
|
|
272
|
+
`WebsocketContext.logMessageContent()` (`WebsocketContext.ts#WebsocketContext.logMessageContent`) :
|
|
273
273
|
|
|
274
|
-
- `string` → tronquée à `WS_LOG_CONTENT_CAP` (4096, `wsLogContent.ts
|
|
274
|
+
- `string` → tronquée à `WS_LOG_CONTENT_CAP` (4096, `wsLogContent.ts#WS_LOG_CONTENT_CAP`) + ellipse.
|
|
275
275
|
- **binaire** (Buffer, ArrayBuffer, TypedArray, Blob, `Buffer[]`) → résumé `[binary N B]`, **jamais**
|
|
276
|
-
sérialisé (`binaryByteLength()`, `wsLogContent.ts
|
|
276
|
+
sérialisé (`binaryByteLength()`, `wsLogContent.ts#binaryByteLength`) — un dump `{"0":..,"1":..}` serait énorme et faux.
|
|
277
277
|
- objet « JSON » → `JSON.stringify` compact tronqué, repli `String(...)` sur cycle/`bigint`.
|
|
278
278
|
|
|
279
279
|
Le tout est **gaté hors production** : `logMessageContent` court-circuite avant toute construction de
|
|
280
|
-
chaîne quand l'env est `production` (`WebsocketContext.ts
|
|
280
|
+
chaîne quand l'env est `production` (`WebsocketContext.ts#WebsocketContext.broadcast`) → 0 surcoût sur le hot path WS.
|
|
281
281
|
|
|
282
282
|
## ⚙️ Configuration
|
|
283
283
|
|
|
284
284
|
Un seul réglage d'app, côté cœur (bloc `log`), pilote le format des lignes.
|
|
285
285
|
|
|
286
|
-
| Option | Type | Défaut | Effet
|
|
287
|
-
| ------------------- | ----------------------------------------- | ------ |
|
|
288
|
-
| `log.requestFormat` | `auto` \| `default` \| `pretty` \| `json` | `auto` | Format de la ligne de bilan. `auto` = pretty en dev / json en prod (`schema.ts
|
|
286
|
+
| Option | Type | Défaut | Effet |
|
|
287
|
+
| ------------------- | ----------------------------------------- | ------ | ---------------------------------------------------------------------------------------------- |
|
|
288
|
+
| `log.requestFormat` | `auto` \| `default` \| `pretty` \| `json` | `auto` | Format de la ligne de bilan. `auto` = pretty en dev / json en prod (`schema.ts#serverSchema`). |
|
|
289
289
|
|
|
290
|
-
`auto` est résolu au boot selon l'environnement (`applyRequestLoggerFromConfig`, `http-kernel.ts
|
|
290
|
+
`auto` est résolu au boot selon l'environnement (`applyRequestLoggerFromConfig`, `http-kernel.ts#HttpKernel.applyRequestLoggerFromConfig`) :
|
|
291
291
|
|
|
292
|
-
| Valeur | Formateur | Rendu
|
|
293
|
-
| --------- | ---------------------- |
|
|
294
|
-
| `pretty` | `PrettyRequestLogger` | 1 ligne colorée `GET 200 /x 12.3ms 127.0.0.1 [a1b2c3d4]` (`pretty-request-logger.ts
|
|
295
|
-
| `json` | `JsonAuditLogger` | 1 objet JSON canonique par requête (`audit-logger.ts
|
|
296
|
-
| `default` | `DefaultRequestLogger` | Format legacy verbeux `URL : … FROM : … ID : <uuid>` (`request-logger.ts
|
|
292
|
+
| Valeur | Formateur | Rendu |
|
|
293
|
+
| --------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
294
|
+
| `pretty` | `PrettyRequestLogger` | 1 ligne colorée `GET 200 /x 12.3ms 127.0.0.1 [a1b2c3d4]` (`pretty-request-logger.ts#PrettyRequestLogger.renderHttp`). |
|
|
295
|
+
| `json` | `JsonAuditLogger` | 1 objet JSON canonique par requête (`audit-logger.ts#JsonAuditLogger`). |
|
|
296
|
+
| `default` | `DefaultRequestLogger` | Format legacy verbeux `URL : … FROM : … ID : <uuid>` (`request-logger.ts#DefaultRequestLogger.renderHttp`). |
|
|
297
297
|
|
|
298
298
|
> [!NOTE]
|
|
299
299
|
> Le **réglage fin** du logger JSON (`sampleRate`, `includeStack`, `maxCauseDepth`, `nominal`) n'est pas
|
|
300
300
|
> un champ déclaré du schéma d'app : il se pose **programmatiquement**, en construisant le logger et en
|
|
301
301
|
> l'injectant — `httpKernel.setRequestLogger(new JsonAuditLogger({ sampleRate: 10 }))` (options :
|
|
302
|
-
> `JsonAuditLoggerOptions`, `audit-logger.ts
|
|
302
|
+
> `JsonAuditLoggerOptions`, `audit-logger.ts#JsonAuditLoggerOptions`). L'override programmatique gagne toujours sur la config.
|
|
303
303
|
|
|
304
304
|
## 🧰 Les trois formateurs (et le contrat)
|
|
305
305
|
|
|
@@ -314,31 +314,31 @@ Choisir en cinq secondes :
|
|
|
314
314
|
### `PrettyRequestLogger` — une ligne pour l'humain
|
|
315
315
|
|
|
316
316
|
Le plus grand gain en dev : `méthode statut url durée remote [id]`, avec couleur du statut (vert 2xx,
|
|
317
|
-
jaune 4xx, rouge 5xx — `colorizeStatus()`, `pretty-request-logger.ts
|
|
318
|
-
premiers caractères (`shortId()`, `pretty-request-logger.ts
|
|
319
|
-
timing (`pretty-request-logger.ts
|
|
317
|
+
jaune 4xx, rouge 5xx — `colorizeStatus()`, `pretty-request-logger.ts#colorizeStatus`) et `requestId` tronqué aux 8
|
|
318
|
+
premiers caractères (`shortId()`, `pretty-request-logger.ts#shortId`). La durée est dérivée des phases de
|
|
319
|
+
timing (`pretty-request-logger.ts#shortId`).
|
|
320
320
|
|
|
321
321
|
### `JsonAuditLogger` — un PDU par requête, pour la machine
|
|
322
322
|
|
|
323
|
-
Émet un `AuditLogEntry` canonique (`audit-logger.ts
|
|
323
|
+
Émet un `AuditLogEntry` canonique (`audit-logger.ts#AuditLogEntry.ts`) : `ts`, `requestId`, `userIdentifier` (appelant authentifié, posé par le pare-feu sur `context.userIdentifier`), `type`, `method`,
|
|
324
324
|
`url`, `status`, `durationMs`, `remoteAddress`, `host`, `userAgent`, phases, erreur enrichie (nom, code,
|
|
325
325
|
`errorType`, `cause` bornée à 5, stack **dev seulement**). Deux propriétés majeures :
|
|
326
326
|
|
|
327
327
|
- **Redaction par construction** : `Authorization` et `Cookie` ne sont **jamais** sérialisés — seuls des
|
|
328
|
-
drapeaux `hasAuthorization`/`hasCookie` le sont (`audit-logger.ts
|
|
329
|
-
- **Sampling déterministe** : `shouldSample()` (`audit-logger.ts
|
|
328
|
+
drapeaux `hasAuthorization`/`hasCookie` le sont (`audit-logger.ts#AuditLogEntry.hasCookie`).
|
|
329
|
+
- **Sampling déterministe** : `shouldSample()` (`audit-logger.ts#JsonAuditLogger.shouldSample`) garde 1 requête nominale sur N mais
|
|
330
330
|
**jamais** une erreur ni un `status >= 400` — on ne perd aucun échec ; compteur, pas de RNG.
|
|
331
331
|
|
|
332
332
|
### `DefaultRequestLogger` — le format legacy
|
|
333
333
|
|
|
334
|
-
Singleton sans état, 0 allocation par requête (`request-logger.ts
|
|
334
|
+
Singleton sans état, 0 allocation par requête (`request-logger.ts#DefaultRequestLogger.renderHttp`). Conserve le format historique
|
|
335
335
|
`URL : … FROM : … ORIGIN : … ID : <uuid>`, avec `Accept-Protocol` en plus côté WebSocket.
|
|
336
336
|
|
|
337
337
|
### Écrire son propre formateur
|
|
338
338
|
|
|
339
|
-
Implémenter `IRequestLogger` (`IRequestLogger.ts
|
|
340
|
-
5424 texte, OpenTelemetry logs… `httpKernel.setRequestLogger(monLogger)` (`http-kernel.ts
|
|
341
|
-
formateurs et le type sont exportés depuis `@nodefony/http` (`index.ts
|
|
339
|
+
Implémenter `IRequestLogger` (`IRequestLogger.ts#IRequestLogger`) et l'injecter — NCSA Common Log Format, syslog RFC
|
|
340
|
+
5424 texte, OpenTelemetry logs… `httpKernel.setRequestLogger(monLogger)` (`http-kernel.ts#HttpKernel.setRequestLogger`). Les trois
|
|
341
|
+
formateurs et le type sont exportés depuis `@nodefony/http` (`index.ts`).
|
|
342
342
|
|
|
343
343
|
## 🔐 Sécurité
|
|
344
344
|
|
|
@@ -348,15 +348,15 @@ sensibles à la fois — d'où une validation stricte.
|
|
|
348
348
|
<!-- prettier-ignore -->
|
|
349
349
|
| Menace | Vecteur | Défense |
|
|
350
350
|
| --- | --- | --- |
|
|
351
|
-
| Log injection (CR/LF) | `X-Request-Id: a\r\nFAKE LINE` écrit tel quel dans les logs | Allowlist `[A-Za-z0-9._-]{1,128}` (`requestId.ts
|
|
352
|
-
| Response splitting / DoS | Caractère de contrôle / non-ASCII → throw `setHeader` natif (500) | Même allowlist ; valeur non conforme **rejetée**, pas nettoyée — `sanitizeRequestId()` (`requestId.ts
|
|
353
|
-
| Log flooding | `X-Request-Id` géant | Borne `MAX_REQUEST_ID_LENGTH = 128` (`requestId.ts
|
|
354
|
-
| Fuite de secret dans l'audit | `Authorization` / `Cookie` sérialisés dans le log JSON | Drapeaux de présence seuls — valeurs jamais écrites (`audit-logger.ts
|
|
355
|
-
| Fuite de stack en prod | `error.stack` dans les logs d'audit publics | `includeStack` par défaut `false` en production (`audit-logger.ts
|
|
351
|
+
| Log injection (CR/LF) | `X-Request-Id: a\r\nFAKE LINE` écrit tel quel dans les logs | Allowlist `[A-Za-z0-9._-]{1,128}` (`requestId.ts#SAFE_REQUEST_ID`) — CR/LF exclus. |
|
|
352
|
+
| Response splitting / DoS | Caractère de contrôle / non-ASCII → throw `setHeader` natif (500) | Même allowlist ; valeur non conforme **rejetée**, pas nettoyée — `sanitizeRequestId()` (`requestId.ts#sanitizeRequestId`). |
|
|
353
|
+
| Log flooding | `X-Request-Id` géant | Borne `MAX_REQUEST_ID_LENGTH = 128` (`requestId.ts#MAX_REQUEST_ID_LENGTH`). |
|
|
354
|
+
| Fuite de secret dans l'audit | `Authorization` / `Cookie` sérialisés dans le log JSON | Drapeaux de présence seuls — valeurs jamais écrites (`audit-logger.ts#JsonAuditLogger.renderHttp`). |
|
|
355
|
+
| Fuite de stack en prod | `error.stack` dans les logs d'audit publics | `includeStack` par défaut `false` en production (`audit-logger.ts#JsonAuditLogger`). |
|
|
356
356
|
|
|
357
357
|
> [!WARNING]
|
|
358
358
|
> On **rejette** plutôt que d'assainir un `requestId` invalide : nettoyer donnerait au client un faux
|
|
359
|
-
> contrôle sur l'identifiant et masquerait la tentative d'abus (`requestId.ts
|
|
359
|
+
> contrôle sur l'identifiant et masquerait la tentative d'abus (`requestId.ts#sanitizeRequestId`).
|
|
360
360
|
|
|
361
361
|
## ⚡ Performance & mémoire
|
|
362
362
|
|
|
@@ -364,15 +364,15 @@ La ligne de bilan et la corrélation sont sur le **chemin chaud** : leur coût e
|
|
|
364
364
|
Les choix visibles dans le code :
|
|
365
365
|
|
|
366
366
|
- **`requestId` gratuit à l'entropie** — `randomUUID()` de Node met en cache l'entropie (128 UUID/appel
|
|
367
|
-
système) ; les ids W3C amortissent de même via un pool de 4096 o (`randomHex()`, `trace.ts
|
|
367
|
+
système) ; les ids W3C amortissent de même via un pool de 4096 o (`randomHex()`, `trace.ts#randomHex`).
|
|
368
368
|
- **Provider ALS lu paresseusement** — un `Pdu` hors bulle ne paie que 1 test de référence ; le provider
|
|
369
|
-
reste `null` côté navigateur/debugbar (`Pdu.ts
|
|
369
|
+
reste `null` côté navigateur/debugbar (`Pdu.ts#guid`) → 0 lecture, 0 allocation.
|
|
370
370
|
- **Sampling avant rendu** — `shouldSample()` saute la requête **avant** `renderHttp` : 0 objet, 0
|
|
371
|
-
`toISOString`, 0 `JSON.stringify`, 0 `Pdu` au ring (`audit-logger.ts
|
|
371
|
+
`toISOString`, 0 `JSON.stringify`, 0 `Pdu` au ring (`audit-logger.ts#JsonAuditLogger.shouldSample`).
|
|
372
372
|
- **Audit nominal coupable** — l'option `nominal` coupe le log des 2xx/3xx si le sink texte est `null`
|
|
373
|
-
(l'entrée n'atteindrait aucune destination) — ~5,9 % du profil CPU récupérés (`audit-logger.ts
|
|
373
|
+
(l'entrée n'atteindrait aucune destination) — ~5,9 % du profil CPU récupérés (`audit-logger.ts#JsonAuditLoggerOptions.sampleRate`).
|
|
374
374
|
- **Trace des frames WS gatée en prod** — `logMessageContent()` court-circuite avant toute construction
|
|
375
|
-
de chaîne hors dev (`WebsocketContext.ts
|
|
375
|
+
de chaîne hors dev (`WebsocketContext.ts#WebsocketContext.logMessageContent`) ; les events lifecycle ne créent aucun `Pdu` en production, et ne sont que DEBUG hors production (`Context.ts#lifecycleEventLogging`).
|
|
376
376
|
|
|
377
377
|
Gate mémoire avant tout commit touchant le pipeline : `npm run test:memory` (skill
|
|
378
378
|
`nodefony-check-memory-health`). Rejouer les chiffres de charge : skill `nodefony-load-test`.
|
|
@@ -387,10 +387,10 @@ Le `requestId` est la **clé de jointure** de l'admin Studio (dev). Les écrans
|
|
|
387
387
|
| **Suivi de requête** (`TraceView`) | `/nodefony/syslog/api/logs/search?requestId=…&order=asc` | Toutes les lignes corrélées + le profil serveur (phases, requêtes SQL). |
|
|
388
388
|
| **Logs** (stream) | data plane syslog | Le flux de logs live, filtrable. |
|
|
389
389
|
| **Audit** | écran Audit | Les événements d'audit persistés. |
|
|
390
|
-
| **Profiler** (dev) | `/nodefony/profiler/api/{requestId}` (`ProfilerAdminApi.ts
|
|
390
|
+
| **Profiler** (dev) | `/nodefony/profiler/api/{requestId}` (`ProfilerAdminApi.ts#createProfilerAdminApi`) | Le profil complet (waterfall des phases) d'une requête donnée. |
|
|
391
391
|
|
|
392
|
-
Le profiler indexe ses instantanés par `requestId` (`Profiler.ts
|
|
393
|
-
`X-Request-Id` de son propre appel AJAX et va chercher le profil (`Profiler.ts
|
|
392
|
+
Le profiler indexe ses instantanés par `requestId` (`Profiler.ts#Profiler.collect`) ; la debug bar lit le
|
|
393
|
+
`X-Request-Id` de son propre appel AJAX et va chercher le profil (`Profiler.ts#ProfilePhase`). Le profiler n'est
|
|
394
394
|
instancié **qu'en dev** (fuite d'info + coût en prod).
|
|
395
395
|
|
|
396
396
|
> [!NOTE]
|
|
@@ -403,28 +403,28 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
|
|
|
403
403
|
<!-- prettier-ignore -->
|
|
404
404
|
| Domaine | Norme | Ancrage |
|
|
405
405
|
| --- | --- | --- |
|
|
406
|
-
| W3C Trace Context (`traceparent`) | W3C Trace Context | `resolveTraceparent()` (`trace.ts
|
|
407
|
-
| Sûreté des valeurs d'en-tête (field-value) | RFC 9110 §5.5 | `sanitizeRequestId()` allowlist (`requestId.ts
|
|
408
|
-
| En-têtes trop volumineux / borne | anti-abus (log flooding) | `MAX_REQUEST_ID_LENGTH` (`requestId.ts
|
|
409
|
-
| Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts
|
|
410
|
-
| Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts
|
|
411
|
-
| Non-journalisation des secrets | OWASP (logging) | redaction présence-only (`audit-logger.ts
|
|
406
|
+
| W3C Trace Context (`traceparent`) | W3C Trace Context | `resolveTraceparent()` (`trace.ts#resolveTraceparent`), `parseTraceparent()` (`trace.ts#parseTraceparent`) |
|
|
407
|
+
| Sûreté des valeurs d'en-tête (field-value) | RFC 9110 §5.5 | `sanitizeRequestId()` allowlist (`requestId.ts#sanitizeRequestId`) |
|
|
408
|
+
| En-têtes trop volumineux / borne | anti-abus (log flooding) | `MAX_REQUEST_ID_LENGTH` (`requestId.ts#MAX_REQUEST_ID_LENGTH`) |
|
|
409
|
+
| Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts#Pdu.requestId`) |
|
|
410
|
+
| Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts#severityFromStatus`) |
|
|
411
|
+
| Non-journalisation des secrets | OWASP (logging) | redaction présence-only (`audit-logger.ts#JsonAuditLogger.renderHttp`) |
|
|
412
412
|
|
|
413
413
|
> `X-Request-Id` n'est **pas** un en-tête normalisé (convention de-facto) ; c'est la **valeur** qu'il
|
|
414
414
|
> transporte qui est soumise à la sûreté RFC 9110 §5.5.
|
|
415
415
|
|
|
416
416
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
417
417
|
|
|
418
|
-
| Symptôme | Cause | Correction
|
|
419
|
-
| --------------------------------------------------- | ------------------------------------------------------------------- |
|
|
420
|
-
| Le `X-Request-Id` que j'envoie n'est pas réfléchi | Valeur non conforme (espace, CR/LF, non-ASCII, > 128) → **rejetée** | Utiliser `[A-Za-z0-9._-]{1,128}` (UUID/nanoid/traceparent OK) — sinon UUID serveur
|
|
421
|
-
| Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS | Déjà géré : l'override `log()` rouvre une micro-bulle (`Context.ts
|
|
422
|
-
| Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1 | Déjà géré (`http2/Response.ts
|
|
423
|
-
| Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts
|
|
424
|
-
| Frame WS binaire loggée en `{"0":..,"1":..}` | Sérialisation naïve d'un Buffer | Déjà géré : résumé `[binary N B]` (`wsLogContent.ts
|
|
425
|
-
| Le format de log ne change pas malgré la config | Un `setRequestLogger(...)` programmatique gagne sur la config | L'override est volontaire (last setter wins) — retirer l'appel, ou le régler
|
|
426
|
-
| Logs d'audit trop volumineux en prod | `stack` sérialisée, ou 100 % des 2xx audités | `includeStack:false` (défaut prod) + `sampleRate` via `setRequestLogger`
|
|
427
|
-
| `Authorization`/`Cookie` attendus dans le log JSON | Redaction : jamais sérialisés | Par conception — lire les drapeaux `hasAuthorization`/`hasCookie`
|
|
418
|
+
| Symptôme | Cause | Correction |
|
|
419
|
+
| --------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
420
|
+
| Le `X-Request-Id` que j'envoie n'est pas réfléchi | Valeur non conforme (espace, CR/LF, non-ASCII, > 128) → **rejetée** | Utiliser `[A-Za-z0-9._-]{1,128}` (UUID/nanoid/traceparent OK) — sinon UUID serveur |
|
|
421
|
+
| Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS | Déjà géré : l'override `log()` rouvre une micro-bulle (`Context.ts#Context.log`) |
|
|
422
|
+
| Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1 | Déjà géré (`http2/Response.ts#Http2Response.writeHead`) — le port 5152 réfléchit aussi |
|
|
423
|
+
| Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts#HttpKernel.handleWebsocket`) |
|
|
424
|
+
| Frame WS binaire loggée en `{"0":..,"1":..}` | Sérialisation naïve d'un Buffer | Déjà géré : résumé `[binary N B]` (`wsLogContent.ts#formatWsLogContent`) |
|
|
425
|
+
| Le format de log ne change pas malgré la config | Un `setRequestLogger(...)` programmatique gagne sur la config | L'override est volontaire (last setter wins) — retirer l'appel, ou le régler |
|
|
426
|
+
| Logs d'audit trop volumineux en prod | `stack` sérialisée, ou 100 % des 2xx audités | `includeStack:false` (défaut prod) + `sampleRate` via `setRequestLogger` |
|
|
427
|
+
| `Authorization`/`Cookie` attendus dans le log JSON | Redaction : jamais sérialisés | Par conception — lire les drapeaux `hasAuthorization`/`hasCookie` |
|
|
428
428
|
|
|
429
429
|
## 🧪 Tests & couverture
|
|
430
430
|
|