@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -2
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +26 -4
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +87 -144
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +22 -15
- package/dist/nodefony/service/trace.js +3 -2
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +108 -46
- package/dist/nodefony/src/context/http/Response.js +134 -54
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +65 -23
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +23 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +42 -57
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/index.md +4 -2
- package/docs/observabilite.md +27 -27
- package/docs/rate-limit.md +20 -20
- package/docs/servers.md +82 -79
- package/docs/session.md +74 -69
- package/docs/upload.md +49 -43
- package/package.json +9 -12
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
|
|
3
|
+
*
|
|
4
|
+
* Même rendu que `String(value)` pour une `Error` (son `message`) et pour les
|
|
5
|
+
* primitives ; un objet quelconque rend son étiquette (`[object Object]`) sans
|
|
6
|
+
* appeler un `toString` arbitraire ni sérialiser son contenu — ce texte peut
|
|
7
|
+
* finir dans une réponse ou un journal, il ne doit rien divulguer.
|
|
8
|
+
*
|
|
9
|
+
* @param value - la valeur levée.
|
|
10
|
+
* @returns un texte court, jamais vide pour une valeur définie.
|
|
11
|
+
*/
|
|
12
|
+
export declare function thrownMessage(value: unknown): string;
|
|
@@ -38,13 +38,13 @@ export interface ProfileQuery {
|
|
|
38
38
|
/** Requête (SQL ou commande NoSQL), tronquée si volumineuse. */
|
|
39
39
|
sql: string;
|
|
40
40
|
/** Début relatif (`performance.now()`) — même horloge que les phases. */
|
|
41
|
-
startMs?: number;
|
|
41
|
+
startMs?: number | undefined;
|
|
42
42
|
/** Durée d'exécution en ms. */
|
|
43
43
|
durationMs: number;
|
|
44
44
|
/** Lignes affectées/retournées, si connu. */
|
|
45
|
-
rows?: number;
|
|
45
|
+
rows?: number | undefined;
|
|
46
46
|
/** Connecteur émetteur (`drizzle`, `mongoose`…). */
|
|
47
|
-
connector?: string;
|
|
47
|
+
connector?: string | undefined;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Traversée du firewall par cette requête — **ce qui était possible** (la zone :
|
|
@@ -97,9 +97,9 @@ export interface ProfileEntry {
|
|
|
97
97
|
error: string | null;
|
|
98
98
|
phases: ProfilePhase[];
|
|
99
99
|
/** Requêtes ORM (SEAM futur — `undefined` tant qu'aucun adapter ne pushe). */
|
|
100
|
-
queries?: ProfileQuery[];
|
|
100
|
+
queries?: ProfileQuery[] | undefined;
|
|
101
101
|
/** Traversée du firewall — `undefined` si la requête n'a croisé aucune zone. */
|
|
102
|
-
security?: ProfileSecurity;
|
|
102
|
+
security?: ProfileSecurity | undefined;
|
|
103
103
|
}
|
|
104
104
|
/** Résumé léger d'un profil pour la liste `recent` (sans les phases). */
|
|
105
105
|
export interface ProfileSummary {
|
|
@@ -115,49 +115,49 @@ export interface ProfileSummary {
|
|
|
115
115
|
}
|
|
116
116
|
/** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
|
|
117
117
|
interface ProfilableContext {
|
|
118
|
-
requestId?: string;
|
|
119
|
-
type?: string;
|
|
120
|
-
scheme?: string;
|
|
121
|
-
method?: string | null;
|
|
122
|
-
url?: string;
|
|
123
|
-
remoteAddress?: string | null;
|
|
124
|
-
traceparent?: string | null;
|
|
118
|
+
requestId?: string | undefined;
|
|
119
|
+
type?: string | undefined;
|
|
120
|
+
scheme?: string | undefined;
|
|
121
|
+
method?: string | null | undefined;
|
|
122
|
+
url?: string | undefined;
|
|
123
|
+
remoteAddress?: string | null | undefined;
|
|
124
|
+
traceparent?: string | null | undefined;
|
|
125
125
|
error?: {
|
|
126
126
|
message?: string;
|
|
127
|
-
} | null;
|
|
127
|
+
} | null | undefined;
|
|
128
128
|
response?: {
|
|
129
129
|
statusCode?: number;
|
|
130
|
-
} | null;
|
|
130
|
+
} | null | undefined;
|
|
131
131
|
phases?: ReadonlyArray<{
|
|
132
132
|
name: string;
|
|
133
133
|
startMs: number;
|
|
134
|
-
durationMs?: number;
|
|
135
|
-
}
|
|
134
|
+
durationMs?: number | undefined;
|
|
135
|
+
}> | undefined;
|
|
136
136
|
resolver?: {
|
|
137
137
|
route?: {
|
|
138
|
-
name?: string;
|
|
138
|
+
name?: string | undefined;
|
|
139
139
|
} | null;
|
|
140
140
|
controller?: {
|
|
141
|
-
name?: string;
|
|
142
|
-
} | null;
|
|
143
|
-
actionName?: string;
|
|
144
|
-
} | null;
|
|
141
|
+
name?: string | undefined;
|
|
142
|
+
} | null | undefined;
|
|
143
|
+
actionName?: string | undefined;
|
|
144
|
+
} | null | undefined;
|
|
145
145
|
/**
|
|
146
146
|
* Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
|
|
147
147
|
* que la payload ALS — les adapters ORM y poussent via `RequestContext`.
|
|
148
148
|
* `null`/absent hors profiling.
|
|
149
149
|
*/
|
|
150
|
-
profilerQueries?: ProfileQuery[] | null;
|
|
150
|
+
profilerQueries?: ProfileQuery[] | null | undefined;
|
|
151
151
|
/**
|
|
152
152
|
* Zone firewall capturée (`SecuredArea`, posée par `Firewall.isSecure`). Lue
|
|
153
153
|
* en structurel : `@nodefony/http` ne peut pas importer `@nodefony/security`.
|
|
154
154
|
*/
|
|
155
155
|
security?: {
|
|
156
|
-
name?: string;
|
|
157
|
-
security?: boolean;
|
|
158
|
-
mode?: string;
|
|
159
|
-
authenticators?: readonly string[];
|
|
160
|
-
} | null;
|
|
156
|
+
name?: string | undefined;
|
|
157
|
+
security?: boolean | undefined;
|
|
158
|
+
mode?: string | undefined;
|
|
159
|
+
authenticators?: readonly string[] | undefined;
|
|
160
|
+
} | null | undefined;
|
|
161
161
|
/** Décision du firewall sur cette requête (dev-only, cf `ISecurityTrace`). */
|
|
162
162
|
securityTrace?: {
|
|
163
163
|
authenticator: string | null;
|
|
@@ -165,7 +165,7 @@ interface ProfilableContext {
|
|
|
165
165
|
reason: string | null;
|
|
166
166
|
user: string | null;
|
|
167
167
|
roles: string[] | null;
|
|
168
|
-
} | null;
|
|
168
|
+
} | null | undefined;
|
|
169
169
|
}
|
|
170
170
|
export declare class Profiler {
|
|
171
171
|
private readonly _buf;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ServersConfig } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Rétrécit la section `servers` de la configuration du kernel (lue dans le sac
|
|
4
|
+
* de config non typé du cœur) en {@link ServersConfig}, sans propager `any`.
|
|
5
|
+
*
|
|
6
|
+
* Seule lecture de `kernel.options.servers` dans `@nodefony/http` : les serveurs
|
|
7
|
+
* et le `HttpKernel` passent tous par ici.
|
|
8
|
+
*
|
|
9
|
+
* @param servers - la valeur brute de `kernel.options.servers`.
|
|
10
|
+
* @returns la section typée, ou `undefined` si elle est absente ou n'est pas un objet.
|
|
11
|
+
*/
|
|
12
|
+
export declare function asServersConfig(servers: unknown): ServersConfig | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
|
|
15
|
+
* serveur est désactivé, absent, ou sans port numérique.
|
|
16
|
+
*
|
|
17
|
+
* @param servers - section `servers` déjà rétrécie ({@link asServersConfig}).
|
|
18
|
+
* @param type - le serveur visé.
|
|
19
|
+
*/
|
|
20
|
+
export declare function configuredServerPort(servers: ServersConfig | undefined, type: "http" | "https"): number | undefined;
|
|
@@ -1,49 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
3
|
-
*
|
|
4
|
-
* Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
|
|
5
|
-
* une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
|
|
6
|
-
* en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
|
|
7
|
-
* un CONTRAT en production.
|
|
8
|
-
*
|
|
9
|
-
* ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
|
|
10
|
-
*
|
|
11
|
-
* La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
|
|
12
|
-
* une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
|
|
13
|
-
* prendre le port — et on échoue quand même, après avoir cru le contraire. Le
|
|
14
|
-
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
15
|
-
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
16
|
-
*
|
|
17
|
-
* ## Pourquoi une SONDE malgré cela
|
|
18
|
-
*
|
|
19
|
-
* Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
|
|
20
|
-
* servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
|
|
21
|
-
* processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
|
|
22
|
-
* liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
|
|
23
|
-
* Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
|
|
24
|
-
* l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
|
|
25
|
-
* rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
|
|
26
|
-
*
|
|
27
|
-
* On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
|
|
28
|
-
* liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
|
|
29
|
-
* outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
|
|
30
|
-
* « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
|
|
31
|
-
* atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
|
|
32
|
-
* que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
|
|
33
|
-
* entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
|
|
34
|
-
*
|
|
35
|
-
* ## Pourquoi sauter les ports réservés
|
|
36
|
-
*
|
|
37
|
-
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
38
|
-
* voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
|
|
39
|
-
* AUTRES serveurs convoitent sont donc sautés d'emblée.
|
|
40
|
-
*
|
|
41
|
-
* ## Fail-loud
|
|
42
|
-
*
|
|
43
|
-
* Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
|
|
44
|
-
* là où on l'attend sans le dire est une dégradation silencieuse.
|
|
45
|
-
*/
|
|
46
1
|
import type { AddressInfo } from "node:net";
|
|
2
|
+
import { resolvePortPolicy } from "nodefony";
|
|
3
|
+
import type { PortPolicy } from "nodefony";
|
|
4
|
+
export { resolvePortPolicy };
|
|
5
|
+
export type { PortPolicy };
|
|
47
6
|
/** Serveur écoutable (surface minimale commune `http`/`https`/`http2`). */
|
|
48
7
|
export interface Listenable {
|
|
49
8
|
listen(port: number, host?: string): unknown;
|
|
@@ -51,8 +10,6 @@ export interface Listenable {
|
|
|
51
10
|
once(event: string, listener: (...args: never[]) => void): unknown;
|
|
52
11
|
removeListener(event: string, listener: (...args: never[]) => void): unknown;
|
|
53
12
|
}
|
|
54
|
-
/** Que faire si le port désiré est occupé. */
|
|
55
|
-
export type PortPolicy = "auto" | "strict";
|
|
56
13
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
57
14
|
export declare const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
58
15
|
export interface BindPlan {
|
|
@@ -69,28 +26,13 @@ export interface BindResult {
|
|
|
69
26
|
/** Port désiré si l'écoute a dû être décalée, `null` si on l'a obtenu. */
|
|
70
27
|
shiftedFrom: number | null;
|
|
71
28
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Politique de port effective.
|
|
74
|
-
*
|
|
75
|
-
* Le défaut dépend de l'environnement, et ce n'est pas de la coquetterie :
|
|
76
|
-
* - **production** → `strict`. Le port y est un contrat (service k8s, ingress,
|
|
77
|
-
* sonde de santé). Un bind silencieux ailleurs donnerait un pod déclaré sain
|
|
78
|
-
* que personne n'atteint : une panne invisible, le pire des deux mondes.
|
|
79
|
-
* - **test** → `strict`. Un port occupé veut dire « un serveur est resté debout » ;
|
|
80
|
-
* le banc doit s'arrêter, pas viser à côté (il taperait le serveur du voisin).
|
|
81
|
-
* - **développement** → `auto`. Ici un port pris n'est qu'une nuisance.
|
|
82
|
-
*
|
|
83
|
-
* @param environment - `kernel.environment` (normalisé `development`/`production`/`test`).
|
|
84
|
-
* @param explicit - `servers.portPolicy` s'il est configuré (il gagne toujours).
|
|
85
|
-
*/
|
|
86
|
-
export declare function resolvePortPolicy(environment: string | undefined, explicit?: PortPolicy): PortPolicy;
|
|
87
29
|
/** Forme lue de `kernel.options.servers` (lecture structurelle, pas d'import core). */
|
|
88
30
|
export interface ServersPortConfig {
|
|
89
31
|
http?: {
|
|
90
|
-
port?: number;
|
|
32
|
+
port?: number | undefined;
|
|
91
33
|
} | false;
|
|
92
34
|
https?: {
|
|
93
|
-
port?: number;
|
|
35
|
+
port?: number | undefined;
|
|
94
36
|
} | false;
|
|
95
37
|
portPolicy?: PortPolicy;
|
|
96
38
|
portRetryAttempts?: number;
|
|
@@ -151,9 +93,18 @@ export declare function detectPortConflict(port: number, host: string | undefine
|
|
|
151
93
|
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
152
94
|
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
153
95
|
*
|
|
96
|
+
* @param server - serveur à mettre en écoute.
|
|
97
|
+
* @param host - hôte passé à `listen`.
|
|
98
|
+
* @param plan - port désiré et nombre de replis permis.
|
|
99
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
100
|
+
* @param sharedListener - vrai dans un worker `node:cluster` : le port y est
|
|
101
|
+
* PARTAGÉ par le maître, et un voisin qui y répond est un frère, pas un
|
|
102
|
+
* conflit. Sans cette exception, `nodefony cluster -w N` ne gardait qu'un
|
|
103
|
+
* worker en vie — les autres mouraient sur « port déjà servi » et
|
|
104
|
+
* bouclaient en relance.
|
|
154
105
|
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
155
106
|
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
156
107
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
157
108
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
158
109
|
*/
|
|
159
|
-
export declare function bindWithFallback(server: Listenable, host: string, plan: BindPlan, probe?: PortListeningProbe): Promise<BindResult>;
|
|
110
|
+
export declare function bindWithFallback(server: Listenable, host: string, plan: BindPlan, probe?: PortListeningProbe, sharedListener?: boolean): Promise<BindResult>;
|
|
@@ -11,24 +11,24 @@ import type { ISession, ISessionStorage, ISerializedSession, SessionStatusType,
|
|
|
11
11
|
* hash chiffré « maison ».
|
|
12
12
|
*/
|
|
13
13
|
export type OptionsSessionType = {
|
|
14
|
-
name?: string;
|
|
15
|
-
cookie?: CookieOptionsType;
|
|
14
|
+
name?: string | undefined;
|
|
15
|
+
cookie?: CookieOptionsType | undefined;
|
|
16
16
|
/** Strict mode : un identifiant inconnu du storage → nouvelle session (anti-fixation). */
|
|
17
|
-
strictMode?: boolean;
|
|
17
|
+
strictMode?: boolean | undefined;
|
|
18
18
|
/** Lie la session à l'hôte (méta `host`) — rejette un changement d'origine. */
|
|
19
|
-
refererCheck?: boolean;
|
|
19
|
+
refererCheck?: boolean | undefined;
|
|
20
20
|
/**
|
|
21
21
|
* Idle timeout (NIST/OWASP, secondes) : inactivité MAX depuis la dernière
|
|
22
22
|
* activité (`updatedAt`, rafraîchi par le touch). `0`/absent = pas d'expiration
|
|
23
23
|
* par inactivité. Enforcement serveur ({@link Session.isValidSession} + GC).
|
|
24
24
|
*/
|
|
25
|
-
idleTimeoutS?: number;
|
|
25
|
+
idleTimeoutS?: number | undefined;
|
|
26
26
|
/**
|
|
27
27
|
* Absolute timeout (OWASP, secondes) : durée de vie MAX depuis la création,
|
|
28
28
|
* indépendante de l'activité. Borne la fenêtre d'exploitation d'un identifiant
|
|
29
29
|
* volé même sur session active. `0`/absent = désactivé (seul l'idle s'applique).
|
|
30
30
|
*/
|
|
31
|
-
absoluteTimeoutS?: number;
|
|
31
|
+
absoluteTimeoutS?: number | undefined;
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
34
|
* Session serveur Nodefony — état persistant lié à une identité, indexé par un
|
|
@@ -59,13 +59,13 @@ declare class Session implements ISession {
|
|
|
59
59
|
* aucune écriture storage.
|
|
60
60
|
*/
|
|
61
61
|
readOnly: boolean;
|
|
62
|
-
context?: ContextType;
|
|
63
|
-
created?: Date;
|
|
64
|
-
updated?: Date;
|
|
62
|
+
context?: ContextType | undefined;
|
|
63
|
+
created?: Date | undefined;
|
|
64
|
+
updated?: Date | undefined;
|
|
65
65
|
options: OptionsSessionType;
|
|
66
66
|
cookieSession: Cookie | null;
|
|
67
|
-
lifetime?: number;
|
|
68
|
-
user?: string;
|
|
67
|
+
lifetime?: number | undefined;
|
|
68
|
+
user?: string | undefined;
|
|
69
69
|
strategy: SessionStrategyType;
|
|
70
70
|
/** Sac d'attributs applicatifs (clé/valeur) — exposé via {@link getAttributes}. */
|
|
71
71
|
private attributesBag;
|
|
@@ -161,7 +161,7 @@ declare class Session implements ISession {
|
|
|
161
161
|
clearFlashBag(key: string): void;
|
|
162
162
|
clearFlashBags(): void;
|
|
163
163
|
serialize(user?: string): ISerializedSession;
|
|
164
|
-
deSerialize(
|
|
164
|
+
deSerialize(serialized: ISerializedSession): void;
|
|
165
165
|
/** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
|
|
166
166
|
clear(): void;
|
|
167
167
|
getName(): string;
|
|
@@ -68,7 +68,7 @@ declare class RevocationGuardStorage implements ISessionStorage {
|
|
|
68
68
|
* Comme ce décorateur est posé en production dès qu'une révocation est
|
|
69
69
|
* possible, l'oubli aurait désactivé le tri **partout**.
|
|
70
70
|
*/
|
|
71
|
-
readonly sortableFields?: readonly string[];
|
|
71
|
+
readonly sortableFields?: readonly string[] | undefined;
|
|
72
72
|
constructor(inner: ISessionStorage);
|
|
73
73
|
read(id: string): Promise<ISerializedSession>;
|
|
74
74
|
start(id: string): Promise<ISerializedSession>;
|
package/docs/cookies.md
CHANGED
|
@@ -116,12 +116,12 @@ les invariants** que le navigateur exigerait de toute façon — pour que l'erre
|
|
|
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:393`) **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
121
|
impose `Secure`, `cookie.ts:393`).
|
|
122
122
|
|
|
123
123
|
**La signature refuse le secret prévisible.** Un cookie `signed: true` sans secret configuré **jette**
|
|
124
|
-
(`setValue()`, `cookie.ts:
|
|
124
|
+
(`setValue()`, `cookie.ts:205`) : signer avec le secret public par défaut ne protégerait rien
|
|
125
125
|
(fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts:380`).
|
|
126
126
|
|
|
127
127
|
**`SameSite` retombe toujours sur `Lax`, jamais sur `None`.** Toute valeur inconnue est normalisée vers
|
|
@@ -223,7 +223,7 @@ Le constructeur accepte `(nom, valeur, options?)` ou **un cookie à copier** (su
|
|
|
223
223
|
| Option | Type | Défaut | Effet |
|
|
224
224
|
| ---------- | -------------------------- | ------------ | ---------------------------------------------------------------------- |
|
|
225
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:
|
|
226
|
+
| `expires` | `Date \| string \| number` | — | Date d'expiration absolue (`setExpires()`, `cookie.ts:272`). |
|
|
227
227
|
| `path` | `string` | `/` | Préfixe d'URL de portée. |
|
|
228
228
|
| `domain` | `string` | `undefined` | Domaine de portée (retiré si nom `__Host-`). |
|
|
229
229
|
| `secure` | `boolean` | `true` | HTTPS only. Forcé si `SameSite=None` ou préfixe `__Host-`/`__Secure-`. |
|
|
@@ -231,18 +231,18 @@ Le constructeur accepte `(nom, valeur, options?)` ou **un cookie à copier** (su
|
|
|
231
231
|
| `sameSite` | `SameSiteType` | `Lax` | `Strict`/`Lax`/`None`. Toute autre valeur retombe sur `Lax`. |
|
|
232
232
|
| `signed` | `boolean` | `false` | Signe la valeur (HMAC). Exige `secret` configuré, sinon **jette**. |
|
|
233
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:
|
|
234
|
+
| `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts:305`). |
|
|
235
235
|
|
|
236
236
|
Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts:39`) ; la fusion se fait dans le
|
|
237
237
|
constructeur de `Cookie` (`cookie.ts:130`).
|
|
238
238
|
|
|
239
239
|
### Sérialiser : `serialize()` et `serializeWebSocket()`
|
|
240
240
|
|
|
241
|
-
- `serialize()` (`cookie.ts:
|
|
241
|
+
- `serialize()` (`cookie.ts:393`) produit la **ligne `Set-Cookie`** complète (attributs dans l'ordre,
|
|
242
242
|
préfixes forcés, `Max-Age` seulement s'il est positif).
|
|
243
|
-
- `serializeWebSocket()` (`cookie.ts:
|
|
243
|
+
- `serializeWebSocket()` (`cookie.ts:435`) produit un **objet** `IWsCookie` (mêmes invariants de sécurité)
|
|
244
244
|
— utilisé quand un cookie doit être décrit hors en-tête HTTP.
|
|
245
|
-
- `toString()` (`cookie.ts:
|
|
245
|
+
- `toString()` (`cookie.ts:325`) ne rend que `nom=valeurEncodée` (sans attributs).
|
|
246
246
|
|
|
247
247
|
### Signer / vérifier : `sign()` et `unsign()`
|
|
248
248
|
|
|
@@ -255,28 +255,28 @@ constructeur de `Cookie` (`cookie.ts:130`).
|
|
|
255
255
|
|
|
256
256
|
| Besoin | Appel | Où |
|
|
257
257
|
| ------------------------------ | ----------------------------------------------------- | ---------------------------------------- |
|
|
258
|
-
| Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts:
|
|
258
|
+
| Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts:674`) |
|
|
259
259
|
| Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts:193`) |
|
|
260
|
-
| Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts:
|
|
261
|
-
| Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:
|
|
260
|
+
| Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts:685`) |
|
|
261
|
+
| Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:213` |
|
|
262
262
|
|
|
263
|
-
Côté réponse HTTP, `addCookie()` (`http/Response.ts:
|
|
264
|
-
(`http/Response.ts:
|
|
263
|
+
Côté réponse HTTP, `addCookie()` (`http/Response.ts:195`) enregistre le cookie, et `setCookies()`
|
|
264
|
+
(`http/Response.ts:221`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
|
|
265
265
|
boucle de `setHeader` (qui écraserait tout sauf le dernier). Pour expirer un cookie chez le client :
|
|
266
|
-
`clearCookie()` (`cookie.ts:
|
|
266
|
+
`clearCookie()` (`cookie.ts:200`) recule `Expires` à l'époque.
|
|
267
267
|
|
|
268
268
|
### Parsing des cookies entrants
|
|
269
269
|
|
|
270
270
|
`cookiesParser(context)` (`cookie.ts:87`) lit l'en-tête `Cookie:` (via la bibliothèque `cookie`,
|
|
271
271
|
`parser()` `cookie.ts:50`), crée un `Cookie` par entrée et l'ajoute au contexte avec `addRequestCookie()`
|
|
272
|
-
(`Context.ts:
|
|
272
|
+
(`Context.ts:664`). Il est déclenché automatiquement par le pipeline : `parseCookies()` est appelé à
|
|
273
273
|
l'initialisation du contexte HTTP (`HttpContext.ts:190`) **et** WebSocket (`WebsocketContext.ts:170`).
|
|
274
274
|
|
|
275
275
|
### Côté WebSocket — lecture oui, écriture non
|
|
276
276
|
|
|
277
277
|
Les cookies **entrants** sont lus au handshake, exactement comme en HTTP (même parseur). Mais la **poignée
|
|
278
278
|
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:
|
|
279
|
+
**no-op** (`websocket/Response.ts:299`), une limite de la bibliothèque `ws`. Le cookie de session, lui, est
|
|
280
280
|
posé pendant la **phase HTTP** qui précède l'upgrade. La forme d'un cookie décrit pour le WS est
|
|
281
281
|
`IWsCookie` (`ICookie.ts:19`), produite par `serializeWebSocket()`.
|
|
282
282
|
|
|
@@ -284,12 +284,12 @@ posé pendant la **phase HTTP** qui précède l'upgrade. La forme d'un cookie d
|
|
|
284
284
|
|
|
285
285
|
Les cookies **applicatifs** ne se configurent pas par schéma : on les construit dans le code, avec les
|
|
286
286
|
défauts sûrs de `cookieDefaultSettings` (`cookie.ts:39`). Le seul cookie **piloté par la config** est celui
|
|
287
|
-
de la **session** — bloc Zod `sessionCookieSchema` (`config.ts:
|
|
287
|
+
de la **session** — bloc Zod `sessionCookieSchema` (`config.ts:752`), avec notamment `hostPrefix`
|
|
288
288
|
(`config.ts:770`) qui décide du préfixe `__Host-`. Tout cela est documenté dans [Sessions](session.md) :
|
|
289
289
|
cette page ne le duplique pas.
|
|
290
290
|
|
|
291
291
|
Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport) est calculé par
|
|
292
|
-
`getSessionCookieName()` (`Context.ts:
|
|
292
|
+
`getSessionCookieName()` (`Context.ts:733`) — encore un détail qui appartient à la page Sessions.
|
|
293
293
|
|
|
294
294
|
## 🛡️ Défenses par attribut
|
|
295
295
|
|
|
@@ -297,19 +297,19 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
|
|
|
297
297
|
| -------------------------- | --------------------------------------- | ------------------------------------------------------------------------ |
|
|
298
298
|
| `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts:39`) |
|
|
299
299
|
| `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts:250`) |
|
|
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:
|
|
300
|
+
| `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts:393`) |
|
|
301
|
+
| Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts:393`) |
|
|
302
302
|
| Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts:355`, `cookie.ts:355`) |
|
|
303
|
-
| Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts:
|
|
303
|
+
| Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts:205`) |
|
|
304
304
|
|
|
305
305
|
## 📜 Normes appliquées
|
|
306
306
|
|
|
307
307
|
| Domaine | Norme | Ancrage |
|
|
308
308
|
| ---------------------------------- | -------------------- | ------------------------------------------------------------------ |
|
|
309
|
-
| Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts:
|
|
309
|
+
| Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts:393`) |
|
|
310
310
|
| `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts:3`), `setSameSite()` (`cookie.ts:250`) |
|
|
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:
|
|
311
|
+
| Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts:393`) |
|
|
312
|
+
| `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts:393`) |
|
|
313
313
|
| Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts:331`) |
|
|
314
314
|
| Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts:380`) |
|
|
315
315
|
|
|
@@ -322,7 +322,7 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
|
|
|
322
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
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
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:
|
|
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:221`) |
|
|
326
326
|
| `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement |
|
|
327
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 |
|
|
328
328
|
|
|
@@ -330,7 +330,7 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
|
|
|
330
330
|
|
|
331
331
|
Il n'y a pas d'écran Studio dédié aux cookies applicatifs (le cookie **de session** est surfacé dans
|
|
332
332
|
l'écran **Sessions**). En développement, chaque écriture de cookie est journalisée en `DEBUG` par la
|
|
333
|
-
réponse HTTP (`ADD COOKIE ==> …`, `setCookie()` `http/Response.ts:
|
|
333
|
+
réponse HTTP (`ADD COOKIE ==> …`, `setCookie()` `http/Response.ts:241`) — visible via le skill
|
|
334
334
|
`nodefony-tail-error-logs` ou le Suivi de requête. Sur le fil, un `curl -i` montre les lignes `Set-Cookie`.
|
|
335
335
|
|
|
336
336
|
## 🧪 Tests & couverture
|
package/docs/index.md
CHANGED
|
@@ -125,8 +125,10 @@ se périmeraient en silence.
|
|
|
125
125
|
## ⚙️ Configuration
|
|
126
126
|
|
|
127
127
|
Tout se déclare dans `nodefony.config.ts` via `use("@nodefony/http", { … })`. Les blocs Zod
|
|
128
|
-
(`nodefony/config/config.ts`) couvrent : `
|
|
129
|
-
`cookie
|
|
128
|
+
(`nodefony/config/config.ts`) couvrent : `http`, `https`, `http2` (transport, TLS), `websocket` / `websocketSecure`, `session`
|
|
129
|
+
(dont `session.cookie`), `trustProxy` et `trustedHosts`, `certificates`, `upload` et `maxBodySize`,
|
|
130
|
+
`rateLimit`, `health` et `statics`. La topologie (`servers` : ports, protocole, politique de port) est
|
|
131
|
+
un bloc de la configuration d'application, pas de ce module. Chaque page
|
|
130
132
|
de brique détaille son bloc et ses défauts réels.
|
|
131
133
|
|
|
132
134
|
## 📜 Normes appliquées
|
package/docs/observabilite.md
CHANGED
|
@@ -112,12 +112,12 @@ un `requestId`, un `traceparent` et un contrat de logger **uniques** couvrent le
|
|
|
112
112
|
|
|
113
113
|
**Le `requestId` est un citoyen du contexte, pas un décor.** Il naît dans le constructeur de base
|
|
114
114
|
`Context.requestId = randomUUID()` (`Context.ts:244`), voyage dans l'ALS via `RequestContext.run(...)`
|
|
115
|
-
(`http-kernel.ts:
|
|
115
|
+
(`http-kernel.ts:1510` pour HTTP, `http-kernel.ts:1929` 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
119
|
`IRequestLogger` (`IRequestLogger.ts:25`) résolu au boot depuis la config (`applyRequestLoggerFromConfig`,
|
|
120
|
-
`http-kernel.ts:
|
|
120
|
+
`http-kernel.ts:682`), remplaçable à chaud par `httpKernel.setRequestLogger(...)` (`http-kernel.ts:910`).
|
|
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,
|
|
@@ -222,20 +222,20 @@ 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ù | Comportement
|
|
226
|
-
| ------------------- | ------------------------------------------------------------------- |
|
|
227
|
-
| Génération | `Context.requestId = randomUUID()` (`Context.ts:244`) | UUID v4 posé dans le constructeur de base — HTTP **et** WS.
|
|
228
|
-
| Adoption HTTP | `sanitizeRequestId(headers["x-request-id"])` (`HttpContext.ts:158`) | Remplace l'UUID **si** la valeur cliente est sûre, sinon on garde l'UUID.
|
|
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:71`) | Sinon les réponses du port 5152 sortiraient sans corrélation.
|
|
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:262`) | Provider injectable branché sur l'ALS côté Node — 0 lecture côté navigateur.
|
|
225
|
+
| Étape | Où | Comportement |
|
|
226
|
+
| ------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| Génération | `Context.requestId = randomUUID()` (`Context.ts:244`) | UUID v4 posé dans le constructeur de base — HTTP **et** WS. |
|
|
228
|
+
| Adoption HTTP | `sanitizeRequestId(headers["x-request-id"])` (`HttpContext.ts:158`) | Remplace l'UUID **si** la valeur cliente est sûre, sinon on garde l'UUID. |
|
|
229
|
+
| Adoption WS | `sanitizeRequestId(...)` au handshake (`WebsocketContext.ts:162`) | 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:248`) | Écrit dans `writeHead()`, sur **chaque** réponse. |
|
|
231
|
+
| Réflexion HTTP/2 | `this.headers["x-request-id"] = requestId` (`http2/Response.ts:71`) | Sinon les réponses du port 5152 sortiraient sans corrélation. |
|
|
232
|
+
| ALS (HTTP) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:1510`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
|
|
233
|
+
| ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:1929`) | 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:262`) | 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:535`) 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
|
|
|
@@ -249,15 +249,15 @@ Nodefony implémente **W3C Trace Context** (le code s'y réfère explicitement,
|
|
|
249
249
|
- La validation refuse `version=ff` et un `traceId`/`spanId` tout-à-zéro — `parseTraceparent()`
|
|
250
250
|
(`trace.ts:38`), 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:551`). 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
254
|
`ws` n'expose pas proprement ce chemin (`http-kernel.ts:1419`) ; 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:
|
|
258
|
+
Le kernel tient un `IRequestLogger` singleton (`http-kernel.ts:289`) et lui délègue le rendu de la ligne
|
|
259
|
+
de bilan, au teardown, via `Context.logRequest()` (`Context.ts:608`) côté HTTP et
|
|
260
|
+
`WebsocketContext.logRequest()` (`WebsocketContext.ts:233`) côté WS. Le contrat a trois méthodes
|
|
261
261
|
(`IRequestLogger.ts:25`) :
|
|
262
262
|
|
|
263
263
|
- `renderHttp(context, error?)` → `{ text, severity, msgid }` remis à `context.log()`.
|
|
@@ -287,19 +287,19 @@ Un seul réglage d'app, côté cœur (bloc `log`), pilote le format des lignes.
|
|
|
287
287
|
| ------------------- | ----------------------------------------- | ------ | ------------------------------------------------------------------------------------ |
|
|
288
288
|
| `log.requestFormat` | `auto` \| `default` \| `pretty` \| `json` | `auto` | Format de la ligne de bilan. `auto` = pretty en dev / json en prod (`schema.ts:39`). |
|
|
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:682`) :
|
|
291
291
|
|
|
292
292
|
| Valeur | Formateur | Rendu |
|
|
293
293
|
| --------- | ---------------------- | ----------------------------------------------------------------------------------------- |
|
|
294
294
|
| `pretty` | `PrettyRequestLogger` | 1 ligne colorée `GET 200 /x 12.3ms 127.0.0.1 [a1b2c3d4]` (`pretty-request-logger.ts:34`). |
|
|
295
|
-
| `json` | `JsonAuditLogger` | 1 objet JSON canonique par requête (`audit-logger.ts:
|
|
295
|
+
| `json` | `JsonAuditLogger` | 1 objet JSON canonique par requête (`audit-logger.ts:136`). |
|
|
296
296
|
| `default` | `DefaultRequestLogger` | Format legacy verbeux `URL : … FROM : … ID : <uuid>` (`request-logger.ts:21`). |
|
|
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:92`). L'override programmatique gagne toujours sur la config.
|
|
303
303
|
|
|
304
304
|
## 🧰 Les trois formateurs (et le contrat)
|
|
305
305
|
|
|
@@ -325,7 +325,7 @@ timing (`pretty-request-logger.ts:116`).
|
|
|
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:
|
|
328
|
+
drapeaux `hasAuthorization`/`hasCookie` le sont (`audit-logger.ts:226`).
|
|
329
329
|
- **Sampling déterministe** : `shouldSample()` (`audit-logger.ts:156`) 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
|
|
|
@@ -337,7 +337,7 @@ Singleton sans état, 0 allocation par requête (`request-logger.ts:21`). Conser
|
|
|
337
337
|
### Écrire son propre formateur
|
|
338
338
|
|
|
339
339
|
Implémenter `IRequestLogger` (`IRequestLogger.ts:25`) et l'injecter — NCSA Common Log Format, syslog RFC
|
|
340
|
-
5424 texte, OpenTelemetry logs… `httpKernel.setRequestLogger(monLogger)` (`http-kernel.ts:
|
|
340
|
+
5424 texte, OpenTelemetry logs… `httpKernel.setRequestLogger(monLogger)` (`http-kernel.ts:910`). Les trois
|
|
341
341
|
formateurs et le type sont exportés depuis `@nodefony/http` (`index.ts:221`).
|
|
342
342
|
|
|
343
343
|
## 🔐 Sécurité
|
|
@@ -389,7 +389,7 @@ Le `requestId` est la **clé de jointure** de l'admin Studio (dev). Les écrans
|
|
|
389
389
|
| **Audit** | écran Audit | Les événements d'audit persistés. |
|
|
390
390
|
| **Profiler** (dev) | `/nodefony/profiler/api/{requestId}` (`ProfilerAdminApi.ts:23`) | 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:
|
|
392
|
+
Le profiler indexe ses instantanés par `requestId` (`Profiler.ts:214`) ; la debug bar lit le
|
|
393
393
|
`X-Request-Id` de son propre appel AJAX et va chercher le profil (`Profiler.ts:14`). Le profiler n'est
|
|
394
394
|
instancié **qu'en dev** (fuite d'info + coût en prod).
|
|
395
395
|
|
|
@@ -407,7 +407,7 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
|
|
|
407
407
|
| Sûreté des valeurs d'en-tête (field-value) | RFC 9110 §5.5 | `sanitizeRequestId()` allowlist (`requestId.ts:38`) |
|
|
408
408
|
| En-têtes trop volumineux / borne | anti-abus (log flooding) | `MAX_REQUEST_ID_LENGTH` (`requestId.ts:18`) |
|
|
409
409
|
| Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts:184`) |
|
|
410
|
-
| Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts:
|
|
410
|
+
| Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts:85`) |
|
|
411
411
|
| Non-journalisation des secrets | OWASP (logging) | redaction présence-only (`audit-logger.ts:211`) |
|
|
412
412
|
|
|
413
413
|
> `X-Request-Id` n'est **pas** un en-tête normalisé (convention de-facto) ; c'est la **valeur** qu'il
|
|
@@ -418,9 +418,9 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
|
|
|
418
418
|
| Symptôme | Cause | Correction |
|
|
419
419
|
| --------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
420
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:
|
|
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:535`) |
|
|
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:106`) — 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:1915`) |
|
|
424
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:63`) |
|
|
425
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
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` |
|