@nodefony/http 10.0.0-beta.1 → 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 +5 -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 +2 -2
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +4 -3
- package/dist/nodefony/service/certificates.js +74 -143
- package/dist/nodefony/service/http-kernel.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/http/Request.js +17 -4
- package/dist/nodefony/src/context/http/Response.js +30 -6
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/service/certificates.d.ts +32 -47
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/docs/cookies.md +10 -10
- package/docs/index.md +4 -2
- package/docs/observabilite.md +13 -13
- package/docs/rate-limit.md +19 -19
- package/docs/servers.md +50 -47
- package/docs/session.md +17 -17
- package/docs/upload.md +29 -29
- package/package.json +7 -10
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type pkg from "node-forge";
|
|
2
1
|
import { Service, Module } from "nodefony";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { type KeyObject } from "node:crypto";
|
|
3
|
+
import { type ICertificateAttribute } from "./x509.js";
|
|
4
|
+
/** Paire de clés RSA du certificat auto-signé. */
|
|
5
|
+
export interface IRsaKeyPair {
|
|
6
|
+
privateKey: KeyObject;
|
|
7
|
+
publicKey: KeyObject;
|
|
8
|
+
}
|
|
9
9
|
/** Algorithmes de hachage de signature autorisés (SHA-1 banni). */
|
|
10
10
|
export type CertHash = "sha256" | "sha384" | "sha512";
|
|
11
11
|
/** Stratégie de fourniture du certificat exposée en configuration. */
|
|
12
12
|
export type CertStrategyConfig = "auto" | "mkcert" | "selfsigned" | "explicit";
|
|
13
13
|
/**
|
|
14
|
-
* Options de génération du certificat AUTO-SIGNÉ (node
|
|
15
|
-
*
|
|
14
|
+
* Options de génération du certificat AUTO-SIGNÉ (`node:crypto` — aucun
|
|
15
|
+
* binaire externe ni aucune dépendance tierce).
|
|
16
16
|
*
|
|
17
17
|
* ⚠️ Portée : ces réglages ne valent QUE pour la stratégie `selfsigned`. Sous
|
|
18
18
|
* `mkcert` (le défaut en développement) ils sont ignorés — mkcert ne reçoit que
|
|
@@ -29,7 +29,7 @@ export interface SelfSignedOptions {
|
|
|
29
29
|
/** Recul de `notBefore` (minutes) — tolérance au décalage d'horloge client. */
|
|
30
30
|
backdateMinutes: number;
|
|
31
31
|
/** Attributs du sujet/issuer (commonName, organizationName…). */
|
|
32
|
-
attrs:
|
|
32
|
+
attrs: ICertificateAttribute[];
|
|
33
33
|
}
|
|
34
34
|
/** Options de génération du certificat TLS en mode développement. */
|
|
35
35
|
export interface CertificateDevOptions {
|
|
@@ -37,7 +37,7 @@ export interface CertificateDevOptions {
|
|
|
37
37
|
* Préférer `mkcert` (CA locale ajoutée au trust store système) pour générer
|
|
38
38
|
* le certificat de dev. Donne un HTTPS sans erreur navigateur — indispensable
|
|
39
39
|
* pour les sous-ressources cross-origin (Vite) et le WSS du HMR.
|
|
40
|
-
* Si `mkcert` est absent, on retombe sur un auto-signé
|
|
40
|
+
* Si `mkcert` est absent, on retombe sur un auto-signé (non trusté).
|
|
41
41
|
* Ignoré hors `development`.
|
|
42
42
|
*/
|
|
43
43
|
useMkcert: boolean;
|
|
@@ -96,7 +96,7 @@ export interface CertificateInfo {
|
|
|
96
96
|
*
|
|
97
97
|
* Trois stratégies : `explicit` (certificat fourni en config — le cas de
|
|
98
98
|
* PRODUCTION), `mkcert` (CA locale trustée, confort de dev) et `selfsigned`
|
|
99
|
-
* (auto-
|
|
99
|
+
* (auto-signé, secours). La génération est réservée au
|
|
100
100
|
* DÉVELOPPEMENT : en production sans certificat fourni, le service crie un
|
|
101
101
|
* avertissement (Nodefony n'est pas une autorité de certification).
|
|
102
102
|
*
|
|
@@ -108,8 +108,7 @@ export interface CertificateInfo {
|
|
|
108
108
|
declare class Certificate extends Service {
|
|
109
109
|
module: Module;
|
|
110
110
|
files: filesCertType[];
|
|
111
|
-
keysPair:
|
|
112
|
-
certForge: pkg.pki.Certificate | null;
|
|
111
|
+
keysPair: IRsaKeyPair | null;
|
|
113
112
|
ca: Buffer | string | null;
|
|
114
113
|
key: Buffer | string | null;
|
|
115
114
|
cert: Buffer | string | null;
|
|
@@ -119,13 +118,6 @@ declare class Certificate extends Service {
|
|
|
119
118
|
intermediateCertPem: Buffer | string | null;
|
|
120
119
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
121
120
|
private mkcertCaRoot;
|
|
122
|
-
/**
|
|
123
|
-
* Backend crypto node-forge, chargé PARESSEUSEMENT. La génération de
|
|
124
|
-
* certificat est un confort de DÉVELOPPEMENT : en production avec un
|
|
125
|
-
* certificat fourni (`strategy: "explicit"`), cette grosse dépendance n'est
|
|
126
|
-
* jamais importée (règle perf-mémoire — pas de dep chargée « au cas où »).
|
|
127
|
-
*/
|
|
128
|
-
private forge;
|
|
129
121
|
path: string;
|
|
130
122
|
serverPath: string;
|
|
131
123
|
caPath: string;
|
|
@@ -138,14 +130,6 @@ declare class Certificate extends Service {
|
|
|
138
130
|
private get certOptions();
|
|
139
131
|
/** Vrai en environnement de développement (mkcert réservé à ce mode). */
|
|
140
132
|
private isDev;
|
|
141
|
-
/**
|
|
142
|
-
* Charge node-forge à la demande (idempotent) — uniquement sur le chemin de
|
|
143
|
-
* GÉNÉRATION (selfsigned / mkcert / inspection). Le chemin `explicit` (prod)
|
|
144
|
-
* ne l'appelle jamais.
|
|
145
|
-
*/
|
|
146
|
-
loadForge(): Promise<ForgeModule>;
|
|
147
|
-
/** Accès au backend forge déjà chargé (lève si `loadForge` n'a pas été appelé). */
|
|
148
|
-
private get forgeLib();
|
|
149
133
|
/**
|
|
150
134
|
* Fabrique-t-on un certificat au démarrage ?
|
|
151
135
|
*
|
|
@@ -182,7 +166,7 @@ declare class Certificate extends Service {
|
|
|
182
166
|
/**
|
|
183
167
|
* Génère (ou recharge) le certificat serveur selon la stratégie résolue :
|
|
184
168
|
* `explicit` (fourni en config / prod), `mkcert` (dev, CA trustée) ou
|
|
185
|
-
* `selfsigned` (auto-
|
|
169
|
+
* `selfsigned` (auto-signé, secours). Régénère automatiquement si
|
|
186
170
|
* le certificat présent sur disque n'est pas adéquat pour la stratégie active
|
|
187
171
|
* (expiré, SHA-1, SAN incomplet).
|
|
188
172
|
*
|
|
@@ -195,7 +179,7 @@ declare class Certificate extends Service {
|
|
|
195
179
|
* erreur si absents.
|
|
196
180
|
* - `mkcert` : dev + binaire mkcert + CA locale présents. Forcé hors dev →
|
|
197
181
|
* retombe sur `selfsigned` avec avertissement.
|
|
198
|
-
* - `selfsigned` : auto-signé
|
|
182
|
+
* - `selfsigned` : auto-signé (fallback). En PRODUCTION, crie un
|
|
199
183
|
* avertissement : la génération n'est pas le rôle d'un serveur de prod.
|
|
200
184
|
*/
|
|
201
185
|
private resolveStrategy;
|
|
@@ -248,30 +232,31 @@ declare class Certificate extends Service {
|
|
|
248
232
|
* par mkcert passe aussi par ici). Échec silencieux hors POSIX.
|
|
249
233
|
*/
|
|
250
234
|
private restrictPrivateKey;
|
|
251
|
-
|
|
235
|
+
/** Génère la paire RSA (taille configurée) hors de la boucle d'événements. */
|
|
236
|
+
generateKeys(): Promise<IRsaKeyPair>;
|
|
237
|
+
/** Clé privée en PEM PKCS#1 (`BEGIN RSA PRIVATE KEY`). */
|
|
252
238
|
generatePrivateKeyPem(): Buffer;
|
|
239
|
+
/** Clé publique en PEM SPKI (`BEGIN PUBLIC KEY`). */
|
|
253
240
|
generatePublickeyPem(): Buffer;
|
|
254
|
-
generateCertPem(): Buffer;
|
|
255
|
-
createCertificate(): pkg.pki.Certificate;
|
|
256
|
-
/** subjectAltName de l'auto-signé : DNS + IP (config explicite sinon dérivé). */
|
|
257
|
-
private altNames;
|
|
258
|
-
setExtension(): void;
|
|
259
241
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
242
|
+
* Fabrique et signe le certificat auto-signé avec la paire courante.
|
|
243
|
+
*
|
|
244
|
+
* Série aléatoire unique (RFC 5280 §4.1.2.2), `notBefore` reculé (décalage
|
|
245
|
+
* d'horloge du client), SAN DNS + IP (config explicite sinon dérivé), signé
|
|
246
|
+
* avec le hachage configuré — jamais SHA-1 (collision SHAttered 2017,
|
|
247
|
+
* CA/Browser Forum depuis 2016).
|
|
248
|
+
*
|
|
249
|
+
* @returns le certificat en PEM
|
|
250
|
+
* @throws Si la paire de clés n'a pas été générée
|
|
264
251
|
*/
|
|
265
|
-
|
|
252
|
+
createCertificate(): Promise<Buffer>;
|
|
266
253
|
/**
|
|
267
254
|
* Résumé introspectable du certificat serveur courant — réutilisé par la
|
|
268
255
|
* commande CLI `certificates` et un futur endpoint d'admin Studio (parité
|
|
269
|
-
* CLI ↔ Web).
|
|
256
|
+
* CLI ↔ Web).
|
|
270
257
|
*/
|
|
271
258
|
describe(): Promise<CertificateInfo>;
|
|
272
|
-
/**
|
|
273
|
-
private
|
|
274
|
-
/** Digest node-forge correspondant au hachage configuré (jamais SHA-1). */
|
|
275
|
-
private digestFor;
|
|
259
|
+
/** Hachage configuré, ramené à SHA-256 s'il sort du contrat (jamais SHA-1). */
|
|
260
|
+
private allowedHash;
|
|
276
261
|
}
|
|
277
262
|
export default Certificate;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fabrique et lecture de certificats X.509 par `node:crypto` seul — aucune
|
|
3
|
+
* dépendance tierce.
|
|
4
|
+
*
|
|
5
|
+
* `node:crypto` sait LIRE un certificat (`X509Certificate`) et SIGNER, pas
|
|
6
|
+
* en FABRIQUER un : ce module encode le `TBSCertificate` en DER (RFC 5280
|
|
7
|
+
* §4.1), le signe avec `crypto.sign`, puis l'assemble. Seul le sous-ensemble
|
|
8
|
+
* nécessaire à un certificat serveur auto-signé est couvert. Les fonctions
|
|
9
|
+
* sont PURES (aucun disque, aucun kernel) : elles s'éprouvent sans décor.
|
|
10
|
+
*
|
|
11
|
+
* Pourquoi pas node-forge : sa vérification de signature RSA PKCS#1 v1.5
|
|
12
|
+
* accepte des signatures forgées (GHSA-86w9-cpqp-85rv), sans aucune version
|
|
13
|
+
* corrigée. Nous ne vérifions aucune signature, mais `npm audit` signale le
|
|
14
|
+
* paquet pour tous ceux qui installent `@nodefony/http`.
|
|
15
|
+
*/
|
|
16
|
+
import { type KeyObject } from "node:crypto";
|
|
17
|
+
/** Hachages de signature que le builder sait déclarer. */
|
|
18
|
+
export type X509Hash = "sha1" | "sha256" | "sha384" | "sha512";
|
|
19
|
+
/**
|
|
20
|
+
* Attribut d'un nom distinctif (sujet / émetteur). Forme reprise de la config
|
|
21
|
+
* historique : `name` long (`commonName`), `shortName` (`CN`) ou `type` (OID
|
|
22
|
+
* pointé) — le premier présent fait foi.
|
|
23
|
+
*/
|
|
24
|
+
export interface ICertificateAttribute {
|
|
25
|
+
name?: string | undefined;
|
|
26
|
+
shortName?: string | undefined;
|
|
27
|
+
type?: string | undefined;
|
|
28
|
+
value?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Ce qu'il faut pour fabriquer un certificat serveur auto-signé. */
|
|
31
|
+
export interface ISelfSignedSpec {
|
|
32
|
+
privateKey: KeyObject;
|
|
33
|
+
publicKey: KeyObject;
|
|
34
|
+
/** Numéro de série en hexadécimal (entier positif, RFC 5280 §4.1.2.2). */
|
|
35
|
+
serialHex: string;
|
|
36
|
+
notBefore: Date;
|
|
37
|
+
notAfter: Date;
|
|
38
|
+
/** Sujet ET émetteur (auto-signé). Vide = nom vide, SAN alors critique. */
|
|
39
|
+
attributes: ICertificateAttribute[];
|
|
40
|
+
dns: string[];
|
|
41
|
+
ip: string[];
|
|
42
|
+
/** `sha1` n'est accepté que pour fabriquer des témoins de test. */
|
|
43
|
+
hash: X509Hash;
|
|
44
|
+
}
|
|
45
|
+
/** OID de la signature RSA-SHA1, que le service régénère. */
|
|
46
|
+
export declare const SHA1_WITH_RSA_OID: string;
|
|
47
|
+
/** Octets d'une adresse IP pour un SAN `iPAddress` (4 ou 16 octets). */
|
|
48
|
+
export declare function ipAddressBytes(ip: string): Buffer;
|
|
49
|
+
/**
|
|
50
|
+
* OID de l'algorithme de signature d'un certificat DER (champ
|
|
51
|
+
* `signatureAlgorithm`, RFC 5280 §4.1.1.2). Lu dans le DER plutôt que par
|
|
52
|
+
* `X509Certificate.signatureAlgorithmOid`, absent des Node du plancher.
|
|
53
|
+
*
|
|
54
|
+
* @param der - certificat encodé DER (`X509Certificate.raw`)
|
|
55
|
+
* @throws Si le DER n'a pas la forme d'un certificat
|
|
56
|
+
*/
|
|
57
|
+
export declare function signatureAlgorithmOid(der: Buffer): string;
|
|
58
|
+
/** Nom lisible d'un OID de signature (l'OID lui-même s'il est inconnu). */
|
|
59
|
+
export declare function signatureAlgorithmName(oid: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Entrées DNS et IP d'un `subjectAltName` tel que le rend
|
|
62
|
+
* `X509Certificate.subjectAltName` (`DNS:a, IP Address:1.2.3.4`).
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseSubjectAltName(value: string | undefined): {
|
|
65
|
+
dns: string[];
|
|
66
|
+
ip: string[];
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Valeur d'un champ d'un nom distinctif tel que le rend
|
|
70
|
+
* `X509Certificate.subject` / `.issuer` (une ligne `CLÉ=valeur` par champ).
|
|
71
|
+
*/
|
|
72
|
+
export declare function distinguishedNameField(dn: string, key: string): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Fabrique un certificat serveur auto-signé et le rend en PEM.
|
|
75
|
+
*
|
|
76
|
+
* Contenu : version 3, sujet = émetteur, `basicConstraints` cA=false,
|
|
77
|
+
* `keyUsage` (digitalSignature, keyEncipherment — critique),
|
|
78
|
+
* `extKeyUsage` (serverAuth, clientAuth), `subjectAltName` (critique si le
|
|
79
|
+
* sujet est vide, RFC 5280 §4.2.1.6) et `subjectKeyIdentifier` (SHA-1 de la
|
|
80
|
+
* clé publique, méthode 1 de RFC 5280 §4.2.1.2).
|
|
81
|
+
*
|
|
82
|
+
* @param spec - clés, série, validité, noms et hachage
|
|
83
|
+
* @returns le certificat encodé PEM
|
|
84
|
+
* @throws Si un attribut ou une adresse IP est invalide
|
|
85
|
+
*/
|
|
86
|
+
export declare function createSelfSignedCertificate(spec: ISelfSignedSpec): Promise<string>;
|
|
@@ -1,48 +1,3 @@
|
|
|
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";
|
|
47
2
|
import { resolvePortPolicy } from "nodefony";
|
|
48
3
|
import type { PortPolicy } from "nodefony";
|
|
@@ -138,9 +93,18 @@ export declare function detectPortConflict(port: number, host: string | undefine
|
|
|
138
93
|
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
139
94
|
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
140
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.
|
|
141
105
|
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
142
106
|
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
143
107
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
144
108
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
145
109
|
*/
|
|
146
|
-
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>;
|
package/docs/cookies.md
CHANGED
|
@@ -255,13 +255,13 @@ 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
266
|
`clearCookie()` (`cookie.ts:200`) recule `Expires` à l'époque.
|
|
267
267
|
|
|
@@ -269,7 +269,7 @@ boucle de `setHeader` (qui écraserait tout sauf le dernier). Pour expirer un co
|
|
|
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
|
|
@@ -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
|
|
|
@@ -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:682`), remplaçable à chaud par `httpKernel.setRequestLogger(...)` (`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,
|
|
@@ -226,16 +226,16 @@ GET 200 /trace/whoami 3.1ms 127.0.0.1 [demo-abc]
|
|
|
226
226
|
| ------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
227
227
|
| Génération | `Context.requestId = randomUUID()` (`Context.ts:244`) | UUID v4 posé dans le constructeur de base — HTTP **et** WS. |
|
|
228
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:
|
|
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
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:
|
|
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
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
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:
|
|
260
|
-
`WebsocketContext.logRequest()` (`WebsocketContext.ts:
|
|
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()`.
|
|
@@ -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é
|
|
@@ -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:
|
|
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
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:
|
|
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` |
|
package/docs/rate-limit.md
CHANGED
|
@@ -55,7 +55,7 @@ flowchart TD
|
|
|
55
55
|
Trois idées à retenir :
|
|
56
56
|
|
|
57
57
|
1. **La clé est l'IP, pas l'utilisateur.** On borne du **trafic**, pas des identités. L'IP est
|
|
58
|
-
résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts:
|
|
58
|
+
résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts:1089`) —
|
|
59
59
|
non falsifiable tant que `trustProxy` n'accorde pas sa confiance à un proxy.
|
|
60
60
|
2. **Le verdict porte tout.** Un seul appel `hit(key)` (`IRateLimitStore.ts:79`) rend un
|
|
61
61
|
`RateLimitVerdict` (`IRateLimitStore.ts:20`) qui contient déjà limite, restant, reset et
|
|
@@ -106,7 +106,7 @@ Trois choix structurent l'implémentation, et chacun est un compromis assumé.
|
|
|
106
106
|
|
|
107
107
|
**Désactivé par défaut — opt-in explicite.** En cloud-native, le plafond par IP est souvent mieux placé
|
|
108
108
|
à l'**ingress/gateway** (il voit tout le trafic, tous les pods, et rejette avant le coût TLS). Le module
|
|
109
|
-
laisse donc `rateLimit` désarmé par défaut (`config.ts:
|
|
109
|
+
laisse donc `rateLimit` désarmé par défaut (`config.ts:1069`) : `null` tant qu'on ne l'active pas → **0
|
|
110
110
|
coût** sur le chemin chaud. On l'active quand on n'a **pas** d'edge devant soi (bare-metal, VPS), ou en
|
|
111
111
|
défense en profondeur.
|
|
112
112
|
|
|
@@ -118,7 +118,7 @@ court intervalle (`MemoryRateLimitStore.ts:29`). Acceptable pour une défense de
|
|
|
118
118
|
_sliding window_ viendrait en option si le besoin s'en fait sentir.
|
|
119
119
|
|
|
120
120
|
**Refoulé avant toute allocation.** Le verdict est rendu **avant** le contexte, la portée DI et l'ALS
|
|
121
|
-
(`http-kernel.ts:
|
|
121
|
+
(`http-kernel.ts:1097`) : un flood coûte un `Map.get` et rien d'autre. Le contrat `hit()` est
|
|
122
122
|
**synchrone** à dessein (`IRateLimitStore.ts:8`) — aucune `Promise`, aucune microtask sur le chemin de
|
|
123
123
|
chaque requête.
|
|
124
124
|
|
|
@@ -147,7 +147,7 @@ export default defineConfig(() => ({
|
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Les trois clés `enabled` / `windowS` / `max` sont **éditables à chaud** (`runtimeMutable`) : le kernel
|
|
150
|
-
reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts:
|
|
150
|
+
reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts:444`).
|
|
151
151
|
|
|
152
152
|
### 2. Observer le 429 et les en-têtes
|
|
153
153
|
|
|
@@ -178,7 +178,7 @@ X-RateLimit-Reset: 1753082460
|
|
|
178
178
|
Retry-After: 42
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
- `X-RateLimit-Remaining` : requêtes restantes dans la fenêtre (`http-kernel.ts:
|
|
181
|
+
- `X-RateLimit-Remaining` : requêtes restantes dans la fenêtre (`http-kernel.ts:1099`).
|
|
182
182
|
- `X-RateLimit-Reset` : **epoch en secondes** de la fin de fenêtre (`http-kernel.ts:1031`).
|
|
183
183
|
- `Retry-After` (sur le `429` seulement) : secondes à attendre, **jamais 0** — un `Retry-After: 0`
|
|
184
184
|
relancerait un client bien élevé immédiatement (`MemoryRateLimitStore.ts:80`).
|
|
@@ -203,12 +203,12 @@ flowchart TD
|
|
|
203
203
|
|
|
204
204
|
Autour de ce cœur, le kernel orchestre le cycle de vie :
|
|
205
205
|
|
|
206
|
-
- **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:
|
|
207
|
-
depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:
|
|
208
|
-
(`http-kernel.ts:
|
|
209
|
-
- **Émission HTTP** : sous le quota, les en-têtes `X-RateLimit-*` sont posés (`http-kernel.ts:
|
|
210
|
-
la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:
|
|
211
|
-
(`http-kernel.ts:
|
|
206
|
+
- **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:444`) instancie le store
|
|
207
|
+
depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:454`) et arme un `GcScheduler`
|
|
208
|
+
(`http-kernel.ts:460`) qui **purge les fenêtres expirées** hors du chemin chaud.
|
|
209
|
+
- **Émission HTTP** : sous le quota, les en-têtes `X-RateLimit-*` sont posés (`http-kernel.ts:1098`) et
|
|
210
|
+
la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:1105`) puis `writeHead(429)`
|
|
211
|
+
(`http-kernel.ts:1110`) — corps vide, on ne journalise pas chaque rejet (amplificateur sous flood).
|
|
212
212
|
- **Borne mémoire** : au cap `maxTracked`, le store purge les expirées puis évince en **FIFO**
|
|
213
213
|
(`#evict`, `MemoryRateLimitStore.ts:169`) — la mémoire ne dérive jamais.
|
|
214
214
|
|
|
@@ -242,19 +242,19 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
|
|
|
242
242
|
Un WebSocket ne peut **pas** recevoir un `429` : au moment où le rate-limit décide, le `101 Switching
|
|
243
243
|
Protocols` est déjà parti sur le fil (émis par la bibliothèque `ws`). Le refoulement se fait donc par
|
|
244
244
|
une **fermeture RFC 6455 `1013 Try Again Later`**, décidée dans `onWebsocketRequest()`
|
|
245
|
-
(`http-kernel.ts:
|
|
245
|
+
(`http-kernel.ts:1848`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
|
|
246
246
|
|
|
247
247
|
Deux plafonds distincts, tous deux par IP forwarded-aware :
|
|
248
248
|
|
|
249
249
|
| Plafond | Ce qu'il borne | Source de config | Refus |
|
|
250
250
|
| ---------------------- | ---------------------------------------------- | ----------------------- | ---------------------------------------------------------- |
|
|
251
|
-
| Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:
|
|
252
|
-
| Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:
|
|
251
|
+
| Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:448`) |
|
|
252
|
+
| Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1882`) |
|
|
253
253
|
|
|
254
254
|
Le cap concurrent est porté par un compteur dédié, `WsConnectionCounter` (`WsConnectionCounter.ts:18`) :
|
|
255
255
|
`tryAcquire(ip)` (`WsConnectionCounter.ts:33`) réserve un créneau à l'upgrade, `release(ip)`
|
|
256
256
|
(`WsConnectionCounter.ts:45`) le rend à la fermeture — branché sur `ws.once("close", …)`
|
|
257
|
-
(`http-kernel.ts:
|
|
257
|
+
(`http-kernel.ts:1887`), donc jamais de fuite de compteur, même sur un `terminate` de heartbeat.
|
|
258
258
|
|
|
259
259
|
> [!WARNING]
|
|
260
260
|
> `wsMaxConnectionsPerIp` a une **portée par process** (1 pod) : il ne voit que le trafic de son propre
|
|
@@ -280,14 +280,14 @@ Un adapter doit fournir : `hit(key)` (verdict de fenêtre), `gc()` (purge), `lis
|
|
|
280
280
|
|
|
281
281
|
| Domaine | Norme | Ancrage |
|
|
282
282
|
| ---------------------------------- | ---------------- | ------------------------------------------------- |
|
|
283
|
-
| `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts:
|
|
284
|
-
| `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:
|
|
285
|
-
| IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:
|
|
283
|
+
| `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts:1110`) |
|
|
284
|
+
| `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:1105`) |
|
|
285
|
+
| IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:1089`) |
|
|
286
286
|
| WebSocket — close `1013` Try Again | RFC 6455 §7.4.1 | refus d'upgrade (`http-kernel.ts:1378`) |
|
|
287
287
|
|
|
288
288
|
> [!NOTE]
|
|
289
289
|
> Les en-têtes émis sont la famille **de facto** `X-RateLimit-Limit/Remaining/Reset`
|
|
290
|
-
> (`http-kernel.ts:
|
|
290
|
+
> (`http-kernel.ts:1098`), largement déployée et lue par les clients. Le brouillon IETF
|
|
291
291
|
> `draft-ietf-httpapi-ratelimit-headers` (en-têtes `RateLimit` / `RateLimit-Policy`) n'est **pas** encore
|
|
292
292
|
> émis — une évolution possible, pas une régression : rien ne le promet aujourd'hui.
|
|
293
293
|
|