@nodefony/http 10.0.0-alpha.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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,372 @@
1
+ ---
2
+ title: "Rate-limit — plafond de trafic par IP (429, close 1013)"
3
+ navTitle: Rate-limit
4
+ lang: fr
5
+ module: "@nodefony/http"
6
+ topic: rate-limit
7
+ section: "Cœur runtime"
8
+ audience: [developer, devops]
9
+ tags:
10
+ [
11
+ rate-limit,
12
+ throttling,
13
+ 429,
14
+ retry-after,
15
+ x-ratelimit,
16
+ fenetre-fixe,
17
+ websocket,
18
+ 1013,
19
+ ddos,
20
+ trust-proxy,
21
+ ]
22
+ version: "doc"
23
+ status: stable
24
+ updated: 2026-07-21
25
+ source: "src/packages/@nodefony/http/docs/rate-limit.md"
26
+ coverageModule: http
27
+ coverageFiles: rateLimit/MemoryRateLimitStore.ts,rateLimit/IRateLimitStore.ts,rateLimit/WsConnectionCounter.ts,http-kernel.ts,HttpAdminApi.ts
28
+ ---
29
+
30
+ # Rate-limit — plafond de trafic par IP (429, close 1013)
31
+
32
+ > Un **tourniquet de métro** posé à l'entrée du processus : chaque IP cliente reçoit un quota de
33
+ > requêtes par fenêtre de temps ; au-delà, on la refoule — un `429 Too Many Requests` en HTTP, une
34
+ > fermeture `1013 Try Again Later` en WebSocket. Le refus se décide **avant** d'allouer le moindre
35
+ > contexte, pour qu'un flood coûte une simple recherche dans une table de hachage. C'est une **défense
36
+ > de capacité par IP**, à ne pas confondre avec le backoff anti-bruteforce du **login** (page voisine,
37
+ > côté sécurité). Chaque fait ci-dessous est ancré sur le code.
38
+
39
+ 📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Rate-limit**
40
+
41
+ ## 🧠 Le modèle mental — une clé, une fenêtre, un verdict
42
+
43
+ Le rate-limit ne connaît qu'**une** question : « cette IP a-t-elle déjà trop parlé dans la fenêtre en
44
+ cours ? ». Tout le reste en découle.
45
+
46
+ ```mermaid
47
+ flowchart TD
48
+ REQ["Requête HTTP<br/>ou handshake WS"] --> IP["IP cliente résolue<br/>forwarded-aware (RFC 7239)"]
49
+ IP --> HIT["rateLimiter.hit(ip)<br/>fenêtre fixe · O(1) · synchrone"]
50
+ HIT -->|"sous le quota"| PIPE["→ pipeline<br/>+ en-têtes X-RateLimit-*"]
51
+ HIT -->|"quota dépassé · HTTP"| R429["429 Too Many Requests<br/>+ Retry-After"]
52
+ HIT -->|"quota dépassé · WS"| C1013["close 1013<br/>Try Again Later"]
53
+ ```
54
+
55
+ Trois idées à retenir :
56
+
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`) —
59
+ non falsifiable tant que `trustProxy` n'accorde pas sa confiance à un proxy.
60
+ 2. **Le verdict porte tout.** Un seul appel `hit(key)` (`IRateLimitStore.ts:79`) rend un
61
+ `RateLimitVerdict` (`IRateLimitStore.ts:20`) qui contient déjà limite, restant, reset et
62
+ `Retry-After` — de quoi émettre les en-têtes sans relire l'état.
63
+ 3. **HTTP et WebSocket partagent le même compteur.** Un upgrade WS **est** une requête HTTP : il passe
64
+ par le même `hit()`, seule la façon de refouler change (429 en HTTP, close 1013 en WS).
65
+
66
+ ## 📖 Lexique
67
+
68
+ | Terme | Sens |
69
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
70
+ | Rate-limit / throttling | Limiter le **débit** entrant : X requêtes autorisées par unité de temps, le reste est refusé. |
71
+ | Fenêtre fixe | _Fixed window_ : un compteur par IP remis à zéro à chaque intervalle (60 s). Simple, O(1), 0 alloc pour une IP connue. |
72
+ | Fenêtre glissante | _Sliding window_ : lissage continu, plus juste aux bords de fenêtre. **Non** implémenté ici (compromis assumé). |
73
+ | Token bucket | Autre algorithme (jetons rechargés à débit constant). **Non** utilisé ici. |
74
+ | Quota / `max` | Nombre de requêtes autorisées par IP et par fenêtre. Au-delà → refus. |
75
+ | `429` | _Too Many Requests_ (RFC 6585 §4) : le code HTTP du refoulement. |
76
+ | `Retry-After` | En-tête indiquant au client **combien de secondes** attendre avant de réessayer. |
77
+ | `X-RateLimit-*` | Famille d'en-têtes de facto (`Limit`, `Remaining`, `Reset`) qui expose l'état du quota au client. |
78
+ | Close `1013` | _Try Again Later_ (RFC 6455 §7.4.1) : le refoulement d'un WebSocket, faute de pouvoir renvoyer un `429`. |
79
+ | IP forwarded-aware | IP cliente réelle reconstituée derrière un proxy via `X-Forwarded-For` / `Forwarded` (RFC 7239), sous contrôle de trust. |
80
+ | `trustProxy` | Réglage qui décide si l'on croit les en-têtes de proxy pour établir l'IP. `false` par défaut (non falsifiable). |
81
+ | `maxTracked` | Borne mémoire : nombre max d'IP suivies simultanément. Au cap → purge des expirées puis éviction FIFO. |
82
+ | GC (garbage collection) | Ici : balayage périodique qui **purge les fenêtres expirées** hors du chemin chaud (`GcScheduler` du core). |
83
+ | Backoff de login (NIST) | Défense **distincte** : ralentir les tentatives d'authentification par identifiant saisi (`security.rateLimit`). |
84
+
85
+ ## Qu'est-ce qu'un rate-limit, et quelle attaque il bloque ?
86
+
87
+ Sans plafond, un seul client peut lancer des milliers de requêtes par seconde et **saturer** le
88
+ processus : famine de l'event-loop, mémoire qui gonfle, latence p99 qui explose pour tous les autres.
89
+ C'est le cœur d'un **déni de service applicatif** (DoS), volontaire ou accidentel (un script en boucle,
90
+ un crawler mal réglé).
91
+
92
+ Le rate-limit **refoule** l'IP fautive avant qu'elle ne coûte cher : elle reçoit un `429` (ou une
93
+ fermeture `1013` en WebSocket) tant qu'elle dépasse son quota, pendant que les autres IP passent
94
+ intactes. Le quota est **isolé par IP** (`rateLimit.test.ts:77`) : une IP saturée n'affecte jamais ses
95
+ voisines.
96
+
97
+ > [!IMPORTANT]
98
+ > Ce rate-limit borne le **trafic par IP sur toutes les routes**. Il ne remplace **pas** le backoff
99
+ > anti-bruteforce du **login** (`security.rateLimit`, par identifiant saisi, norme NIST) : ce sont deux
100
+ > briques différentes, à deux étages différents. Confondre les deux laisse un trou. Voir
101
+ > [@nodefony/security](../../security/docs/index.md).
102
+
103
+ ## La vision Nodefony
104
+
105
+ Trois choix structurent l'implémentation, et chacun est un compromis assumé.
106
+
107
+ **Désactivé par défaut — opt-in explicite.** En cloud-native, le plafond par IP est souvent mieux placé
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:830`) : `null` tant qu'on ne l'active pas → **0
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
+ défense en profondeur.
112
+
113
+ **Fenêtre fixe, en mémoire, O(1).** L'algorithme est le plus frugal possible :
114
+ `MemoryRateLimitStore` (`MemoryRateLimitStore.ts:33`) tient une entrée `{ count, resetAt }` par IP,
115
+ remise à zéro **en place** à l'expiration (0 allocation pour une IP récurrente). Le prix de cette
116
+ simplicité est connu : un pic à cheval sur deux fenêtres peut laisser passer jusqu'à `2 × max` sur un
117
+ court intervalle (`MemoryRateLimitStore.ts:29`). Acceptable pour une défense de **capacité** ; un
118
+ _sliding window_ viendrait en option si le besoin s'en fait sentir.
119
+
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
122
+ **synchrone** à dessein (`IRateLimitStore.ts:8`) — aucune `Promise`, aucune microtask sur le chemin de
123
+ chaque requête.
124
+
125
+ ## 🚀 Démarrage rapide
126
+
127
+ Dans une application générée par `nodefony create app`, le rate-limit est **présent mais désarmé**. On
128
+ l'active dans le manifeste, via `use("@nodefony/http", { … })`. L'exemple ci-dessous fixe un quota
129
+ volontairement **bas** (5 req/min) pour voir le `429` en quelques secondes.
130
+
131
+ ### 1. Activer et régler
132
+
133
+ ```typescript
134
+ // nodefony.config.ts — activer le rate-limit général par IP
135
+ export default defineConfig(() => ({
136
+ modules: [
137
+ use("@nodefony/http", {
138
+ rateLimit: {
139
+ enabled: true, // opt-in : désarmé par défaut
140
+ windowS: 60, // fenêtre fixe de 60 secondes
141
+ max: 5, // 5 requêtes / IP / fenêtre (bas exprès, pour la démo)
142
+ },
143
+ }),
144
+ "@nodefony/framework",
145
+ ],
146
+ }));
147
+ ```
148
+
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:322`).
151
+
152
+ ### 2. Observer le 429 et les en-têtes
153
+
154
+ Chaque réponse porte l'état du quota ; la 6ᵉ requête dépasse `max=5` et se fait refouler. Les en-têtes
155
+ sont posés avant le routage, donc visibles même sur un 404.
156
+
157
+ ```bash
158
+ # 6 requêtes rapides depuis la même IP → la 6ᵉ prend un 429
159
+ for i in $(seq 1 6); do
160
+ curl -s -o /dev/null -D - http://127.0.0.1:5151/ \
161
+ | grep -iE 'HTTP/|X-RateLimit|Retry-After'
162
+ done
163
+ ```
164
+
165
+ Ce qu'on observe : les cinq premières passent, le compteur `X-RateLimit-Remaining` décroît, puis la
166
+ sixième bascule.
167
+
168
+ ```text
169
+ HTTP/1.1 200 OK
170
+ X-RateLimit-Limit: 5
171
+ X-RateLimit-Remaining: 4
172
+ X-RateLimit-Reset: 1753082460
173
+ ...
174
+ HTTP/1.1 429 Too Many Requests
175
+ X-RateLimit-Limit: 5
176
+ X-RateLimit-Remaining: 0
177
+ X-RateLimit-Reset: 1753082460
178
+ Retry-After: 42
179
+ ```
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`).
183
+ - `Retry-After` (sur le `429` seulement) : secondes à attendre, **jamais 0** — un `Retry-After: 0`
184
+ relancerait un client bien élevé immédiatement (`MemoryRateLimitStore.ts:80`).
185
+
186
+ ## 🏗️ Architecture interne — le parcours d'un `hit`
187
+
188
+ La logique de fenêtre fixe tient dans `hit()` (`MemoryRateLimitStore.ts:51`). Trois cas, tous O(1) :
189
+
190
+ ```mermaid
191
+ flowchart TD
192
+ H["hit(ip)"] --> Q{"IP déjà suivie ?"}
193
+ Q -->|"non"| NEW["nouvelle fenêtre<br/>count=1 (éviction au cap d'abord)"]
194
+ Q -->|"oui"| EXP{"fenêtre expirée ?<br/>now ≥ resetAt"}
195
+ EXP -->|"oui"| RST["reset EN PLACE<br/>count=1 · 0 alloc"]
196
+ EXP -->|"non"| INC["count += 1"]
197
+ INC --> OVER{"count > max ?"}
198
+ OVER -->|"non"| OK["verdict: autorisé<br/>remaining = max − count"]
199
+ OVER -->|"oui"| REJ["verdict: limité<br/>rejectedTotal++ · Retry-After ≥ 1"]
200
+ NEW --> OK
201
+ RST --> OK
202
+ ```
203
+
204
+ Autour de ce cœur, le kernel orchestre le cycle de vie :
205
+
206
+ - **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:412`) instancie le store
207
+ depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:412`) 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).
212
+ - **Borne mémoire** : au cap `maxTracked`, le store purge les expirées puis évince en **FIFO**
213
+ (`#evict`, `MemoryRateLimitStore.ts:169`) — la mémoire ne dérive jamais.
214
+
215
+ ## ⚙️ Configuration
216
+
217
+ Table dérivée de `rateLimitSchema` (`config.ts:847`). Tout est optionnel : ce sont les défauts du
218
+ schéma, écrits ici pour les montrer.
219
+
220
+ | Option | Type | Défaut | Effet | Chaud |
221
+ | ------------- | ------------ | --------- | -------------------------------------------------------------------------------- | ----- |
222
+ | `enabled` | bool | `false` | Arme le rate-limit (HTTP **et** handshakes WS, même compteur) (`config.ts:849`). | oui |
223
+ | `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts:860`). | oui |
224
+ | `max` | int | `300` | Requêtes/IP/fenêtre ; au-delà `429` + `Retry-After` (`config.ts:871`). | oui |
225
+ | `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts:883`). | non |
226
+ | `gcIntervalS` | int (s) | `300` | Intervalle du balayage de purge des fenêtres expirées, hors hot-path. | non |
227
+ | `gcJitter` | bool | `true` | Étale le tick GC d'un jitter aléatoire (anti-thundering-herd multi-pod). | non |
228
+
229
+ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
230
+
231
+ | Option | Type | Défaut | Effet | Chaud |
232
+ | ----------------------- | ------------- | ------ | --------------------------------------------------------------------------------------------------- | ----- |
233
+ | `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade fermé en `1013` (`config.ts:1046`). | oui |
234
+
235
+ > [!TIP]
236
+ > `max: 300` sur `windowS: 60` = **5 req/s soutenu** par IP, avec des rafales tolérées jusqu'à 300 d'un
237
+ > coup. Règle simple : `max` doit couvrir le **pic légitime** d'un vrai utilisateur (rechargement,
238
+ > préchargement d'assets), pas la moyenne — sinon on refoule ses propres clients.
239
+
240
+ ## 🔌 Rate-limit côté WebSocket
241
+
242
+ Un WebSocket ne peut **pas** recevoir un `429` : au moment où le rate-limit décide, le `101 Switching
243
+ Protocols` est déjà parti sur le fil (émis par la bibliothèque `ws`). Le refoulement se fait donc par
244
+ une **fermeture RFC 6455 `1013 Try Again Later`**, décidée dans `onWebsocketRequest()`
245
+ (`http-kernel.ts:1505`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
246
+
247
+ Deux plafonds distincts, tous deux par IP forwarded-aware :
248
+
249
+ | Plafond | Ce qu'il borne | Source de config | Refus |
250
+ | ---------------------- | ---------------------------------------------- | ----------------------- | ---------------------------------------------------------- |
251
+ | Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:1521`) |
252
+ | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1531`) |
253
+
254
+ Le cap concurrent est porté par un compteur dédié, `WsConnectionCounter` (`WsConnectionCounter.ts:18`) :
255
+ `tryAcquire(ip)` (`WsConnectionCounter.ts:33`) réserve un créneau à l'upgrade, `release(ip)`
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.
258
+
259
+ > [!WARNING]
260
+ > `wsMaxConnectionsPerIp` a une **portée par process** (1 pod) : il ne voit que le trafic de son propre
261
+ > worker. Un vrai plafond **global par IP** se fait à l'ingress (`nginx limit_conn`, HAProxy
262
+ > `sc_conn_cur`, annotation k8s). En cloud-native, laisser `null` et déléguer à l'edge ; ne l'activer
263
+ > (ex. `20`) qu'en défense en profondeur sur une machine **sans** ingress.
264
+
265
+ Ces limites-là bornent le **rythme d'ouverture** et le **nombre de sockets**. Elles sont distinctes des
266
+ bornes **par message** (taille `maxPayload` → close `1009`, backpressure), décrites dans
267
+ [Serveurs](servers.md).
268
+
269
+ ## 🧩 Étendre — un store distribué
270
+
271
+ Tout passe par le contrat `IRateLimitStore` (`IRateLimitStore.ts:74`). L'implémentation par défaut est
272
+ en mémoire (par process), mais le contrat est pensé pour un futur backend **distribué** (Redis,
273
+ multi-pod) : `hit()` reste synchrone (le hot-path ne tolère pas de `Promise`), tandis que l'introspection
274
+ `listPage()` (`IRateLimitStore.ts:99`) est asynchrone — un store distribué la servira par `SCAN`.
275
+
276
+ Un adapter doit fournir : `hit(key)` (verdict de fenêtre), `gc()` (purge), `listPage(query)`
277
+ (introspection admin), plus les métriques `trackedCount` et `rejectedTotal` (`IRateLimitStore.ts:101`).
278
+
279
+ ## 📜 Normes appliquées
280
+
281
+ | Domaine | Norme | Ancrage |
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`) |
286
+ | WebSocket — close `1013` Try Again | RFC 6455 §7.4.1 | refus d'upgrade (`http-kernel.ts:1378`) |
287
+
288
+ > [!NOTE]
289
+ > Les en-têtes émis sont la famille **de facto** `X-RateLimit-Limit/Remaining/Reset`
290
+ > (`http-kernel.ts:1000`), largement déployée et lue par les clients. Le brouillon IETF
291
+ > `draft-ietf-httpapi-ratelimit-headers` (en-têtes `RateLimit` / `RateLimit-Policy`) n'est **pas** encore
292
+ > émis — une évolution possible, pas une régression : rien ne le promet aujourd'hui.
293
+
294
+ ## ⚡ Performance & mémoire
295
+
296
+ Le rate-limit vit sur le **chemin chaud absolu** — il s'exécute avant tout le reste, sur chaque requête.
297
+ Les choix visibles dans le code :
298
+
299
+ - **Synchrone, O(1)** : `hit()` = 1 `Map.get` + arithmétique, zéro `Promise`, zéro microtask
300
+ (`IRateLimitStore.ts:8`).
301
+ - **Lazy** : la `Map` interne n'est allouée qu'au **premier** hit (`MemoryRateLimitStore.ts:33`) →
302
+ quand le rate-limit est désarmé (défaut), le coût mémoire est **nul**.
303
+ - **0 alloc pour une IP connue** : la fenêtre expirée se réinitialise **en place**, pas de nouvel objet.
304
+ - **Rejet avant allocation** : un flood est refoulé avant le contexte / la portée DI / l'ALS
305
+ (`http-kernel.ts:860`) → un attaquant paie une recherche de hachage, pas un pipeline complet.
306
+ - **Résolution d'IP seulement si un limiteur est armé** côté WS (`http-kernel.ts:1368`).
307
+ - **Mémoire bornée** : `maxTracked` + éviction FIFO ; purge périodique `unref` hors hot-path.
308
+
309
+ Rejouer la pression sous charge : skill `nodefony-load-test`. Gate mémoire avant tout commit touchant le
310
+ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
311
+
312
+ ## 📡 Observabilité — Studio
313
+
314
+ Le data plane admin expose **qui martèle** via `createHttpAdminApi()` (`HttpAdminApi.ts:141`) :
315
+
316
+ - **`GET /nodefony/http/api/rate-limit/list`** (`HttpAdminApi.ts:218`) — les IP suivies, **les plus
317
+ bruyantes d'abord** (tri `count` décroissant), paginé **côté serveur** (`?limited&q&limit&offset`).
318
+ `q` filtre par **préfixe** d'IP (un sous-réseau `10.0.`), pas par sous-chaîne.
319
+ - **Réservé `ROLE_NODEFONY_ADMIN`** (`HttpAdminApi.ts:220`) : une IP est une **donnée personnelle** →
320
+ seul l'état du compteur sort d'ici, jamais l'URL, l'en-tête ou le corps des requêtes.
321
+ - **État honnête quand désarmé** : rate-limit désactivé (le défaut) → `enabled: false` + liste vide, pas
322
+ un `503` (`HttpAdminApi.ts:241`). La console affiche « désarmé » plutôt qu'une erreur.
323
+ - Métriques exposées : `trackedCount` (IP suivies) et `rejectedTotal` (429 cumulés depuis le boot).
324
+
325
+ ## ⚠️ Pièges (symptôme → cause → correction)
326
+
327
+ | Symptôme | Cause | Correction |
328
+ | ------------------------------------------------------ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
329
+ | Le rate-limit « ne fait rien » | Désactivé par défaut (opt-in) | `rateLimit.enabled: true` dans `use("@nodefony/http", …)` |
330
+ | Toutes les IP derrière le proxy comptent comme **une** | `trustProxy: false` → l'IP vue est celle du proxy, pas du client | Régler `trustProxy` sur l'IP/CIDR du proxy — voir [Serveurs](servers.md) |
331
+ | Un burst laisse passer près de `2 × max` | Limite **assumée** de la fenêtre fixe (pic à cheval sur deux fenêtres) | Réduire `windowS`, ou attendre l'option _sliding window_ |
332
+ | Un WebSocket ne reçoit jamais de `429` | Le `101` est déjà émis — impossible de renvoyer un code HTTP | Attendu : le refus WS est une fermeture `1013` (`http-kernel.ts:1378`) |
333
+ | `wsMaxConnectionsPerIp` semble inefficace en cluster | Portée **par process** — chaque pod compte pour lui | Déléguer le cap global/IP à l'ingress (nginx `limit_conn`, HAProxy) |
334
+ | Confusion avec le lockout de login | `security.rateLimit` = backoff NIST **par identifiant**, brique distincte | Ce sont deux étages différents — cf [@nodefony/security](../../security/docs/index.md) |
335
+ | Une IP « fantôme » n'est jamais comptée | `resolveForwarded()` renvoie `null` (aucun socket fiable) | Attendu : on ne compte jamais sous une clé `null` (qui deviendrait un DoS) |
336
+
337
+ ## 🧪 Tests & couverture
338
+
339
+ Les chiffres exacts vivent dans la carte de tests de cette page (régénérée depuis vitest, jamais figés
340
+ dans le Markdown).
341
+
342
+ | Type | Où |
343
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
344
+ | Unitaires — store | `unit/rateLimit.test.ts` — fenêtre fixe, isolation par IP, borne mémoire + éviction FIFO, `listPage` (tri, filtres, préfixe) |
345
+ | Unitaires — cap WS | `unit/wsConnectionCounter.test.ts` — acquire/refus au plafond, `release`, auto-borne (pas de GC), IP indépendantes |
346
+ | Unitaires — admin | `unit/rateLimitAdminApi.test.ts` — `rate-limit/list` : état désarmé honnête, tri décroissant, `?limited`/`?q`/`?offset`, `ROLE_NODEFONY_ADMIN` |
347
+ | Intégration — WS | `websockets/websocket-limits.test.ts` — bornes de message (taille `maxPayload` → `1009`, séquence, protocole) |
348
+
349
+ Ce qui **manque** aujourd'hui :
350
+
351
+ - Aucun test d'**intégration** ne prouve, sur un serveur vivant, le `429` HTTP **et** les en-têtes
352
+ `X-RateLimit-*` de bout en bout (le comportement du store est prouvé unitairement, son câblage kernel
353
+ ne l'est pas).
354
+ - Aucun test d'intégration ne couvre la **fermeture `1013`** (débit de handshakes et cap concurrent) :
355
+ le compteur est prouvé unitairement, le refus WS de bout en bout est exercé par les bancs E2E
356
+ (`nodefony-load-test` → `run.sh ws-handshake-rl` / `ws-conn-cap`), pas par une suite du module.
357
+ - Pas de banc de **charge dédié** au coût du rate-limit sur le hot-path.
358
+
359
+ Suites : `npm test` (unitaires, serveur non requis), `npm run test:integration` (serveur requis).
360
+ Couverture : `npm run coverage` dans `@nodefony/http` — le pourcentage vit dans le rapport vitest,
361
+ jamais figé ici. Skills associés : `nodefony-load-test`, `nodefony-check-memory-health`,
362
+ `nodefony-security-review`.
363
+
364
+ ## 🔗 Pour aller plus loin
365
+
366
+ - ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
367
+ - 🧭 **Pages sœurs** : [Serveurs](servers.md) — bornes par message (`maxPayload` → `1009`, backpressure), TLS, arrêt gracieux.
368
+ - L'IP cliente derrière un proxy (`trustProxy`, `X-Forwarded-*`) → [Serveurs](servers.md).
369
+ - Le backoff anti-bruteforce du **login** (brique distincte) → [@nodefony/security](../../security/docs/index.md).
370
+ - Où le rate-limit se branche dans le trajet d'une requête → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md).
371
+ - Déployer derrière un ingress qui porte le cap global (probes, TLS, edge) → [docker-cloud-native](../../../../../docs/guides/docker-cloud-native.md).
372
+ - Configuration d'application (`defineConfig`, `use`, env) → [configuration](../../../../../docs/guides/configuration.md).