@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +4 -2
  3. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  4. package/dist/nodefony/command/certificatesCommand.js +1 -1
  5. package/dist/nodefony/command/networkCommand.js +1 -1
  6. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  7. package/dist/nodefony/config/config.js +22 -1
  8. package/dist/nodefony/interfaces/IRouting.js +1 -0
  9. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  10. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  11. package/dist/nodefony/service/audit-logger.js +16 -13
  12. package/dist/nodefony/service/certificates.js +25 -13
  13. package/dist/nodefony/service/error-renderer.js +11 -8
  14. package/dist/nodefony/service/http-kernel.js +340 -163
  15. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  16. package/dist/nodefony/service/servers/clientError.js +2 -2
  17. package/dist/nodefony/service/servers/server-http.js +20 -20
  18. package/dist/nodefony/service/servers/server-https.js +38 -39
  19. package/dist/nodefony/service/servers/server-static.js +13 -17
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
  21. package/dist/nodefony/service/servers/server-websocket.js +8 -7
  22. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  23. package/dist/nodefony/service/sessions/sessions-service.js +19 -12
  24. package/dist/nodefony/service/trace.js +1 -1
  25. package/dist/nodefony/service/upload/upload-service.js +8 -7
  26. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  27. package/dist/nodefony/src/context/Context.js +34 -19
  28. package/dist/nodefony/src/context/forwarded.js +4 -4
  29. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  30. package/dist/nodefony/src/context/http/Request.js +93 -44
  31. package/dist/nodefony/src/context/http/Response.js +107 -51
  32. package/dist/nodefony/src/context/http/parser.js +9 -10
  33. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  34. package/dist/nodefony/src/context/http2/Response.js +53 -25
  35. package/dist/nodefony/src/context/metaData.js +1 -1
  36. package/dist/nodefony/src/context/trustProxy.js +4 -3
  37. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  38. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  39. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  40. package/dist/nodefony/src/cookies/cookie.js +3 -3
  41. package/dist/nodefony/src/errors/httpError.js +6 -6
  42. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  43. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  44. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  45. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  46. package/dist/nodefony/src/servers/portBinder.js +8 -21
  47. package/dist/nodefony/src/session/session.js +18 -10
  48. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  49. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/nodefony/config/config.d.ts +22 -0
  52. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  53. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  54. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  55. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  56. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  57. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  58. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  59. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  60. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  61. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  62. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  63. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  64. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  65. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  66. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  67. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  68. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  69. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  71. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  72. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  73. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  74. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  76. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  77. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  78. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  79. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  80. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  81. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  82. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  83. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  84. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  85. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  86. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  87. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  88. package/docs/cookies.md +20 -20
  89. package/docs/observabilite.md +25 -25
  90. package/docs/rate-limit.md +18 -18
  91. package/docs/servers.md +52 -52
  92. package/docs/session.md +65 -60
  93. package/docs/upload.md +36 -30
  94. package/package.json +7 -7
@@ -44,6 +44,10 @@
44
44
  * là où on l'attend sans le dire est une dégradation silencieuse.
45
45
  */
46
46
  import type { AddressInfo } from "node:net";
47
+ import { resolvePortPolicy } from "nodefony";
48
+ import type { PortPolicy } from "nodefony";
49
+ export { resolvePortPolicy };
50
+ export type { PortPolicy };
47
51
  /** Serveur écoutable (surface minimale commune `http`/`https`/`http2`). */
48
52
  export interface Listenable {
49
53
  listen(port: number, host?: string): unknown;
@@ -51,8 +55,6 @@ export interface Listenable {
51
55
  once(event: string, listener: (...args: never[]) => void): unknown;
52
56
  removeListener(event: string, listener: (...args: never[]) => void): unknown;
53
57
  }
54
- /** Que faire si le port désiré est occupé. */
55
- export type PortPolicy = "auto" | "strict";
56
58
  /** Nombre de ports essayés après le désiré, en `auto`. */
57
59
  export declare const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
58
60
  export interface BindPlan {
@@ -69,28 +71,13 @@ export interface BindResult {
69
71
  /** Port désiré si l'écoute a dû être décalée, `null` si on l'a obtenu. */
70
72
  shiftedFrom: number | null;
71
73
  }
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
74
  /** Forme lue de `kernel.options.servers` (lecture structurelle, pas d'import core). */
88
75
  export interface ServersPortConfig {
89
76
  http?: {
90
- port?: number;
77
+ port?: number | undefined;
91
78
  } | false;
92
79
  https?: {
93
- port?: number;
80
+ port?: number | undefined;
94
81
  } | false;
95
82
  portPolicy?: PortPolicy;
96
83
  portRetryAttempts?: number;
@@ -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(data: ISerializedSession): void;
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:383`) **réécrit** la sortie — `Secure` ajouté, `Path=/` imposé, `Domain` retiré — pour que le
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:199`) : signer avec le secret public par défaut ne protégerait rien
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:264`). |
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:297`). |
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:383`) produit la **ligne `Set-Cookie`** complète (attributs dans l'ordre,
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:425`) produit un **objet** `IWsCookie` (mêmes invariants de sécurité)
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:317`) ne rend que `nom=valeurEncodée` (sans attributs).
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
 
@@ -258,12 +258,12 @@ constructeur de `Cookie` (`cookie.ts:130`).
258
258
  | Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts:660`) |
259
259
  | Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts:193`) |
260
260
  | Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts:667`) |
261
- | Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:119` |
261
+ | Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:156` |
262
262
 
263
- Côté réponse HTTP, `addCookie()` (`http/Response.ts:101`) enregistre le cookie, et `setCookies()`
264
- (`http/Response.ts:127`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
263
+ Côté réponse HTTP, `addCookie()` (`http/Response.ts:138`) enregistre le cookie, et `setCookies()`
264
+ (`http/Response.ts:164`) é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:194`) recule `Expires` à l'époque.
266
+ `clearCookie()` (`cookie.ts:200`) recule `Expires` à l'époque.
267
267
 
268
268
  ### Parsing des cookies entrants
269
269
 
@@ -276,7 +276,7 @@ l'initialisation du contexte HTTP (`HttpContext.ts:190`) **et** WebSocket (`Webs
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:295`), une limite de la bibliothèque `ws`. Le cookie de session, lui, est
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
 
@@ -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:383`) |
301
- | Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts:383`) |
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:199`) |
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:383`) |
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:383`) |
312
- | `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts:383`) |
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:127`) |
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:164`) |
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:146`) — visible via le skill
333
+ réponse HTTP (`ADD COOKIE ==> …`, `setCookie()` `http/Response.ts:184`) — 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
@@ -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:435` pour HTTP, `http-kernel.ts:435` pour WS), et se lit de n'importe où avec
115
+ (`http-kernel.ts:455` pour HTTP, `http-kernel.ts:455` 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:660`), remplaçable à chaud par `httpKernel.setRequestLogger(...)` (`http-kernel.ts:866`).
120
+ `http-kernel.ts:682`), remplaçable à chaud par `httpKernel.setRequestLogger(...)` (`http-kernel.ts:888`).
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:139`) | 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:153`) | É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:436`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
233
- | ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:436`) | Handshake **et** messages (via `AsyncResource.bind`, BUG-001). |
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:139`) | 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:191`) | É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:455`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
233
+ | ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:455`) | 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:459`) rouvre alors une micro-bulle depuis
238
+ > refermée). L'override `Context.log()` (`Context.ts:520`) 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,13 +249,13 @@ 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:435`). Côté
252
+ Le `traceparent` résolu est propagé en ALS **et** réfléchi sur la réponse HTTP (`context/http/Response.ts:494`). 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:270`) et lui délègue le rendu de la ligne
258
+ Le kernel tient un `IRequestLogger` singleton (`http-kernel.ts:289`) et lui délègue le rendu de la ligne
259
259
  de bilan, au teardown, via `Context.logRequest()` (`Context.ts:595`) côté HTTP et
260
260
  `WebsocketContext.logRequest()` (`WebsocketContext.ts:209`) côté WS. Le contrat a trois méthodes
261
261
  (`IRequestLogger.ts:25`) :
@@ -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:660`) :
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:122`). |
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:78`). L'override programmatique gagne toujours sur la config.
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:211`).
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:866`). Les trois
340
+ 5424 texte, OpenTelemetry logs… `httpKernel.setRequestLogger(monLogger)` (`http-kernel.ts:888`). 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:203`) ; la debug bar lit le
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:71`) |
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:459`) |
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:71`) — 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:1348`) |
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:520`) |
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:1698`) |
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` |
@@ -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:991`) —
58
+ résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts:1020`) —
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
@@ -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:860`) : un flood coûte un `Map.get` et rien d'autre. Le contrat `hit()` est
121
+ (`http-kernel.ts:1019`) : 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:416`).
150
+ reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts:433`).
151
151
 
152
152
  ### 2. Observer le 429 et les en-têtes
153
153
 
@@ -178,8 +178,8 @@ 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:1001`).
182
- - `X-RateLimit-Reset` : **epoch en secondes** de la fin de fenêtre (`http-kernel.ts:1003`).
181
+ - `X-RateLimit-Remaining` : requêtes restantes dans la fenêtre (`http-kernel.ts:1030`).
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`).
185
185
 
@@ -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:417`) instancie le store
207
- depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:417`) et arme un `GcScheduler`
208
- (`http-kernel.ts:422`) 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:1000`) et
210
- la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:1007`) puis `writeHead(429)`
211
- (`http-kernel.ts:1012`) — corps vide, on ne journalise pas chaque rejet (amplificateur sous flood).
206
+ - **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:433`) instancie le store
207
+ depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:433`) et arme un `GcScheduler`
208
+ (`http-kernel.ts:443`) 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:1029`) et
210
+ la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:1036`) puis `writeHead(429)`
211
+ (`http-kernel.ts:1041`) — 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:1549`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
245
+ (`http-kernel.ts:1619`) — **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:1574`) |
252
- | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1583`) |
251
+ | Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:1638`) |
252
+ | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1649`) |
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:1536`), donc jamais de fuite de compteur, même sur un `terminate` de heartbeat.
257
+ (`http-kernel.ts:1652`), 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,9 +280,9 @@ 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:1012`) |
284
- | `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:1007`) |
285
- | IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:991`) |
283
+ | `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts:1041`) |
284
+ | `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:1036`) |
285
+ | IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:1020`) |
286
286
  | WebSocket — close `1013` Try Again | RFC 6455 §7.4.1 | refus d'upgrade (`http-kernel.ts:1378`) |
287
287
 
288
288
  > [!NOTE]