@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.5

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 (138) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +13 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +3 -3
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  12. package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
  13. package/dist/nodefony/service/audit-logger.js +23 -30
  14. package/dist/nodefony/service/certificates.js +97 -13
  15. package/dist/nodefony/service/error-renderer.js +29 -12
  16. package/dist/nodefony/service/http-kernel.js +198 -50
  17. package/dist/nodefony/service/pretty-request-logger.js +15 -13
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +3 -3
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +3 -3
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  29. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +78 -43
  36. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  37. package/dist/nodefony/src/context/forwarded.js +1 -2
  38. package/dist/nodefony/src/context/http/HttpContext.js +28 -6
  39. package/dist/nodefony/src/context/http/Request.js +33 -29
  40. package/dist/nodefony/src/context/http/Response.js +23 -6
  41. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +41 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/requestTrace.js +104 -0
  49. package/dist/nodefony/src/context/responseEnded.js +1 -2
  50. package/dist/nodefony/src/context/timingMode.js +21 -0
  51. package/dist/nodefony/src/context/transport.js +45 -0
  52. package/dist/nodefony/src/context/trustProxy.js +1 -2
  53. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  54. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  55. package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
  56. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  57. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  58. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  59. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  60. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  61. package/dist/nodefony/src/cookies/cookie.js +3 -7
  62. package/dist/nodefony/src/errors/httpError.js +1 -2
  63. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  64. package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
  65. package/dist/nodefony/src/profiler/Profiler.js +104 -32
  66. package/dist/nodefony/src/profiler/aggregate.js +442 -0
  67. package/dist/nodefony/src/proxy/forward.js +29 -5
  68. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  69. package/dist/nodefony/src/proxy/rules.js +12 -6
  70. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  71. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  72. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  73. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  74. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  75. package/dist/nodefony/src/servers/portBinder.js +41 -10
  76. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  77. package/dist/nodefony/src/session/session.js +2 -3
  78. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  79. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  80. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  81. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  82. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  83. package/dist/types/index.d.ts +9 -3
  84. package/dist/types/nodefony/config/config.d.ts +4 -0
  85. package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
  86. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  87. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  88. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  89. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  91. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
  92. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  93. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  94. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  95. package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
  96. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  97. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  98. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  99. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  100. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  101. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  102. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  103. package/dist/types/nodefony/src/context/Context.d.ts +38 -5
  104. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
  105. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  106. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  107. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  108. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  109. package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
  110. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  111. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  112. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  113. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
  118. package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
  119. package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
  120. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  121. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  122. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  123. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  124. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  125. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  126. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  127. package/docs/cookies.md +85 -78
  128. package/docs/diagnostics-channel.md +303 -0
  129. package/docs/index.md +6 -1
  130. package/docs/observabilite.md +159 -81
  131. package/docs/rate-limit.md +83 -83
  132. package/docs/reverse-proxy.md +72 -70
  133. package/docs/servers.md +338 -224
  134. package/docs/session.md +179 -179
  135. package/docs/sse.md +24 -20
  136. package/docs/upload.md +120 -117
  137. package/package.json +8 -9
  138. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: "Rate-limit — plafond de trafic par IP (429, close 1013)"
2
+ title: "Rate-limit — plafond de trafic par IP (429, HTTP et handshake WebSocket)"
3
3
  navTitle: Rate-limit
4
4
  lang: fr
5
5
  module: "@nodefony/http"
@@ -15,7 +15,7 @@ tags:
15
15
  x-ratelimit,
16
16
  fenetre-fixe,
17
17
  websocket,
18
- 1013,
18
+ handshake,
19
19
  ddos,
20
20
  trust-proxy,
21
21
  ]
@@ -27,11 +27,11 @@ coverageModule: http
27
27
  coverageFiles: rateLimit/MemoryRateLimitStore.ts,rateLimit/IRateLimitStore.ts,rateLimit/WsConnectionCounter.ts,http-kernel.ts,HttpAdminApi.ts
28
28
  ---
29
29
 
30
- # Rate-limit — plafond de trafic par IP (429, close 1013)
30
+ # Rate-limit — plafond de trafic par IP (429, HTTP et handshake WebSocket)
31
31
 
32
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
33
+ > requêtes par fenêtre de temps ; au-delà, on la refoule — un `429 Too Many Requests`, en HTTP comme
34
+ > au handshake d'un WebSocket (avant le `101` : la socket ne s'ouvre jamais). Le refus se décide **avant** d'allouer le moindre
35
35
  > contexte, pour qu'un flood coûte une simple recherche dans une table de hachage. C'est une **défense
36
36
  > de capacité par IP**, à ne pas confondre avec le backoff anti-bruteforce du **login** (page voisine,
37
37
  > côté sécurité). Chaque fait ci-dessous est ancré sur le code.
@@ -49,19 +49,19 @@ flowchart TD
49
49
  IP --> HIT["rateLimiter.hit(ip)<br/>fenêtre fixe · O(1) · synchrone"]
50
50
  HIT -->|"sous le quota"| PIPE["→ pipeline<br/>+ en-têtes X-RateLimit-*"]
51
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"]
52
+ HIT -->|"quota dépassé · handshake WS"| H429["429 au handshake<br/>avant le 101, aucune socket"]
53
53
  ```
54
54
 
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:1173`) —
58
+ résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts#HttpKernel.dispatchHttpRequest`) —
59
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
60
+ 2. **Le verdict porte tout.** Un seul appel `hit(key)` (`IRateLimitStore.ts#IRateLimitStore.hit`) rend un
61
+ `RateLimitVerdict` (`IRateLimitStore.ts#RateLimitVerdict`) qui contient déjà limite, restant, reset et
62
62
  `Retry-After` — de quoi émettre les en-têtes sans relire l'état.
63
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).
64
+ par le même `hit()`, et il est refoulé de la même façon : un `429`, avant que le `101` ne parte.
65
65
 
66
66
  ## 📖 Lexique
67
67
 
@@ -75,7 +75,7 @@ Trois idées à retenir :
75
75
  | `429` | _Too Many Requests_ (RFC 6585 §4) : le code HTTP du refoulement. |
76
76
  | `Retry-After` | En-tête indiquant au client **combien de secondes** attendre avant de réessayer. |
77
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`. |
78
+ | Handshake refusé | Refus d'un upgrade WebSocket par un statut HTTP AVANT le `101` (RFC 6455 §4.2.2) : la socket ne s'ouvre jamais. |
79
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
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
81
  | `maxTracked` | Borne mémoire : nombre max d'IP suivies simultanément. Au cap → purge des expirées puis éviction FIFO. |
@@ -89,9 +89,9 @@ processus : famine de l'event-loop, mémoire qui gonfle, latence p99 qui explose
89
89
  C'est le cœur d'un **déni de service applicatif** (DoS), volontaire ou accidentel (un script en boucle,
90
90
  un crawler mal réglé).
91
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
92
+ Le rate-limit **refoule** l'IP fautive avant qu'elle ne coûte cher : elle reçoit un `429` (au handshake,
93
+ pour un 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`) : une IP saturée n'affecte jamais ses
95
95
  voisines.
96
96
 
97
97
  > [!IMPORTANT]
@@ -106,20 +106,20 @@ 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:1247`) : `null` tant qu'on ne l'active pas → **0
109
+ laisse donc `rateLimit` désarmé par défaut (`config.ts#httpConfigSchema`) : `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
 
113
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,
114
+ `MemoryRateLimitStore` (`MemoryRateLimitStore.ts#MemoryRateLimitStore`) tient une entrée `{ count, resetAt }` par IP,
115
115
  remise à zéro **en place** à l'expiration (0 allocation pour une IP récurrente). Le prix de cette
116
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
117
+ court intervalle (`MemoryRateLimitStore.ts#MemoryRateLimitStore`). Acceptable pour une défense de **capacité** ; un
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:1172`) : 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
121
+ (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) : un flood coûte un `Map.get` et rien d'autre. Le contrat `hit()` est
122
+ **synchrone** à dessein (`IRateLimitStore.ts#IRateLimitStore.hit`) — aucune `Promise`, aucune microtask sur le chemin de
123
123
  chaque requête.
124
124
 
125
125
  ## 🚀 Démarrage rapide
@@ -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:497`).
150
+ reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts#HttpKernel.configureRateLimit`).
151
151
 
152
152
  ### 2. Observer le 429 et les en-têtes
153
153
 
@@ -178,14 +178,14 @@ 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:1182`).
182
- - `X-RateLimit-Reset` : **epoch en secondes** de la fin de fenêtre (`http-kernel.ts:1182`).
181
+ - `X-RateLimit-Remaining` : requêtes restantes dans la fenêtre (`http-kernel.ts#HttpKernel.dispatchHttpRequest`).
182
+ - `X-RateLimit-Reset` : **epoch en secondes** de la fin de fenêtre (`http-kernel.ts#HttpKernel.dispatchHttpRequest`).
183
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`).
184
+ relancerait un client bien élevé immédiatement (`MemoryRateLimitStore.ts#MemoryRateLimitStore.hit`).
185
185
 
186
186
  ## 🏗️ Architecture interne — le parcours d'un `hit`
187
187
 
188
- La logique de fenêtre fixe tient dans `hit()` (`MemoryRateLimitStore.ts:51`). Trois cas, tous O(1) :
188
+ La logique de fenêtre fixe tient dans `hit()` (`MemoryRateLimitStore.ts#MemoryRateLimitStore.hit`). Trois cas, tous O(1) :
189
189
 
190
190
  ```mermaid
191
191
  flowchart TD
@@ -203,34 +203,34 @@ 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:497`) instancie le store
207
- depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:466`) et arme un `GcScheduler`
208
- (`http-kernel.ts:502`) 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:1182`) et
210
- la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:1189`) puis `writeHead(429)`
211
- (`http-kernel.ts:1194`) — corps vide, on ne journalise pas chaque rejet (amplificateur sous flood).
206
+ - **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts#HttpKernel.configureRateLimit`) instancie le store
207
+ depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts#HttpKernel.onConfigChanged`) et arme un `GcScheduler`
208
+ (`http-kernel.ts#HttpKernel.configureRateLimit`) 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#HttpKernel.dispatchHttpRequest`) et
210
+ la requête continue ; au-delà, `Retry-After` (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) puis `writeHead(429)`
211
+ (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) — 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
- (`#evict`, `MemoryRateLimitStore.ts:169`) — la mémoire ne dérive jamais.
213
+ (`#evict`, `MemoryRateLimitStore.ts#MemoryRateLimitStore.#evict`) — la mémoire ne dérive jamais.
214
214
 
215
215
  ## ⚙️ Configuration
216
216
 
217
- Table dérivée de `rateLimitSchema` (`config.ts:889`). Tout est optionnel : ce sont les défauts du
217
+ Table dérivée de `rateLimitSchema` (`config.ts#rateLimitSchema`). Tout est optionnel : ce sont les défauts du
218
218
  schéma, écrits ici pour les montrer.
219
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:891`). | oui |
223
- | `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts:902`). | oui |
224
- | `max` | int | `300` | Requêtes/IP/fenêtre ; au-delà `429` + `Retry-After` (`config.ts:913`). | oui |
225
- | `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts:925`). | 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 |
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#rateLimitSchema`). | oui |
223
+ | `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts#rateLimitSchema`). | oui |
224
+ | `max` | int | `300` | Requêtes/IP/fenêtre ; au-delà `429` + `Retry-After` (`config.ts#rateLimitSchema`). | oui |
225
+ | `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts#rateLimitSchema`). | 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
228
 
229
229
  Et un réglage **séparé**, propre au WebSocket, à la racine du module :
230
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:1250`). | oui |
231
+ | Option | Type | Défaut | Effet | Chaud |
232
+ | ----------------------- | ------------- | ------ | --------------------------------------------------------------------------------------------------------------- | ----- |
233
+ | `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade refusé en `429` (`config.ts#httpConfigSchema`). | oui |
234
234
 
235
235
  > [!TIP]
236
236
  > `max: 300` sur `windowS: 60` = **5 req/s soutenu** par IP, avec des rafales tolérées jusqu'à 300 d'un
@@ -239,22 +239,22 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
239
239
 
240
240
  ## 🔌 Rate-limit côté WebSocket
241
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:1964`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
242
+ Un WebSocket est refoulé comme une requête HTTP : par un **`429` au handshake**, AVANT que le
243
+ `101 Switching Protocols` ne parte (RFC 6455 §4.2.2) — aucune socket ne s'ouvre, le client voit un
244
+ échec d'ouverture. Le refus se décide en tête de `onWebsocketUpgrade()` (`http-kernel.ts#HttpKernel.onWebsocketUpgrade`) —
245
+ **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
- | 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:471`) |
252
- | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:2240`) |
249
+ | Plafond | Ce qu'il borne | Source de config | Refus |
250
+ | ---------------------- | ---------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------- |
251
+ | Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | `429` au handshake (`http-kernel.ts#HttpKernel.onConfigChanged`) |
252
+ | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | `429` au handshake — `tryAcquire` refuse (`http-kernel.ts#HttpKernel.websocketQuotaRefusal`) |
253
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:1918`), donc jamais de fuite de compteur, même sur un `terminate` de heartbeat.
254
+ Le cap concurrent est porté par un compteur dédié, `WsConnectionCounter` (`WsConnectionCounter.ts#WsConnectionCounter`) :
255
+ `tryAcquire(ip)` (`WsConnectionCounter.ts#WsConnectionCounter.tryAcquire`) réserve un créneau à l'upgrade, `release(ip)`
256
+ (`WsConnectionCounter.ts#WsConnectionCounter.release`) le rend à la fermeture — branché sur `ws.once("close", …)`
257
+ (`http-kernel.ts#HttpKernel.createWebsocketContext`), 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
@@ -268,26 +268,26 @@ bornes **par message** (taille `maxPayload` → close `1009`, backpressure), dé
268
268
 
269
269
  ## 🧩 Étendre — un store distribué
270
270
 
271
- Tout passe par le contrat `IRateLimitStore` (`IRateLimitStore.ts:74`). L'implémentation par défaut est
271
+ Tout passe par le contrat `IRateLimitStore` (`IRateLimitStore.ts#IRateLimitStore`). L'implémentation par défaut est
272
272
  en mémoire (par process), mais le contrat est pensé pour un futur backend **distribué** (Redis,
273
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`.
274
+ `listPage()` (`IRateLimitStore.ts#IRateLimitStore.listPage`) est asynchrone — un store distribué la servira par `SCAN`.
275
275
 
276
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`).
277
+ (introspection admin), plus les métriques `trackedCount` et `rejectedTotal` (`IRateLimitStore.ts#IRateLimitStore.rejectedTotal`).
278
278
 
279
279
  ## 📜 Normes appliquées
280
280
 
281
- | Domaine | Norme | Ancrage |
282
- | ---------------------------------- | ---------------- | ------------------------------------------------- |
283
- | `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts:1194`) |
284
- | `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:1189`) |
285
- | IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:1173`) |
286
- | WebSocket — close `1013` Try Again | RFC 6455 §7.4.1 | refus d'upgrade (`http-kernel.ts:1409`) |
281
+ | Domaine | Norme | Ancrage |
282
+ | ------------------------------ | ---------------- | --------------------------------------------------------------------------- |
283
+ | `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) |
284
+ | `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) |
285
+ | IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts#HttpKernel.dispatchHttpRequest`) |
286
+ | Refus d'un handshake WebSocket | RFC 6455 §4.2.2 | `429` avant le `101` (`http-kernel.ts#HttpKernel.onWebsocketUpgrade`) |
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:1129`), largement déployée et lue par les clients. Le brouillon IETF
290
+ > (`http-kernel.ts#HttpKernel.dispatchHttpRequest`), 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
 
@@ -297,13 +297,13 @@ Le rate-limit vit sur le **chemin chaud absolu** — il s'exécute avant tout le
297
297
  Les choix visibles dans le code :
298
298
 
299
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`) →
300
+ (`IRateLimitStore.ts`).
301
+ - **Lazy** : la `Map` interne n'est allouée qu'au **premier** hit (`MemoryRateLimitStore.ts#MemoryRateLimitStore`) →
302
302
  quand le rate-limit est désarmé (défaut), le coût mémoire est **nul**.
303
303
  - **0 alloc pour une IP connue** : la fenêtre expirée se réinitialise **en place**, pas de nouvel objet.
304
304
  - **Rejet avant allocation** : un flood est refoulé avant le contexte / la portée DI / l'ALS
305
- (`http-kernel.ts:872`) → 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:1399`).
305
+ (`http-kernel.ts#HttpKernel.prepareFrontController`) → 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#HttpKernel.teardownHttp`).
307
307
  - **Mémoire bornée** : `maxTracked` + éviction FIFO ; purge périodique `unref` hors hot-path.
308
308
 
309
309
  Rejouer la pression sous charge : skill `nodefony-load-test`. Gate mémoire avant tout commit touchant le
@@ -311,28 +311,28 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
311
311
 
312
312
  ## 📡 Observabilité — Studio
313
313
 
314
- Le data plane admin expose **qui martèle** via `createHttpAdminApi()` (`HttpAdminApi.ts:141`) :
314
+ Le data plane admin expose **qui martèle** via `createHttpAdminApi()` (`HttpAdminApi.ts#createHttpAdminApi`) :
315
315
 
316
- - **`GET /nodefony/http/api/rate-limit/list`** (`HttpAdminApi.ts:218`) — les IP suivies, **les plus
316
+ - **`GET /nodefony/http/api/rate-limit/list`** (`HttpAdminApi.ts#createHttpAdminApi`) — les IP suivies, **les plus
317
317
  bruyantes d'abord** (tri `count` décroissant), paginé **côté serveur** (`?limited&q&limit&offset`).
318
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** →
319
+ - **Réservé `ROLE_NODEFONY_ADMIN`** (`HttpAdminApi.ts#createHttpAdminApi`) : une IP est une **donnée personnelle** →
320
320
  seul l'état du compteur sort d'ici, jamais l'URL, l'en-tête ou le corps des requêtes.
321
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.
322
+ un `503` (`HttpAdminApi.ts#createHttpAdminApi`). La console affiche « désarmé » plutôt qu'une erreur.
323
323
  - Métriques exposées : `trackedCount` (IP suivies) et `rejectedTotal` (429 cumulés depuis le boot).
324
324
 
325
325
  ## ⚠️ Pièges (symptôme → cause → correction)
326
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:1409`) |
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) |
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 échoue à s'ouvrir sous charge (`1006`) | Débit ou cap de connexions dépassé : `429` au handshake, que le navigateur cache | Attendu : `NodefonySocket` le lit par sa sonde (`429` = transitoire, il retente) |
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
336
 
337
337
  ## 🧪 Tests & couverture
338
338
 
@@ -351,7 +351,7 @@ Ce qui **manque** aujourd'hui :
351
351
  - Aucun test d'**intégration** ne prouve, sur un serveur vivant, le `429` HTTP **et** les en-têtes
352
352
  `X-RateLimit-*` de bout en bout (le comportement du store est prouvé unitairement, son câblage kernel
353
353
  ne l'est pas).
354
- - Aucun test d'intégration ne couvre la **fermeture `1013`** (débit de handshakes et cap concurrent) :
354
+ - Aucun test d'intégration ne couvre le **refus `429` au handshake** (débit de handshakes et cap concurrent) :
355
355
  le compteur est prouvé unitairement, le refus WS de bout en bout est exercé par les bancs E2E
356
356
  (`nodefony-load-test` → `run.sh ws-handshake-rl` / `ws-conn-cap`), pas par une suite du module.
357
357
  - Pas de banc de **charge dédié** au coût du rate-limit sur le hot-path.