@nodefony/http 10.0.0-beta.1 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +48 -7
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +214 -158
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +9 -4
- package/dist/nodefony/src/context/http/Response.js +46 -7
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +17 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +126 -50
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +9 -3
- package/docs/observabilite.md +18 -18
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +115 -78
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +37 -37
- package/package.json +10 -13
package/docs/rate-limit.md
CHANGED
|
@@ -55,7 +55,7 @@ flowchart TD
|
|
|
55
55
|
Trois idées à retenir :
|
|
56
56
|
|
|
57
57
|
1. **La clé est l'IP, pas l'utilisateur.** On borne du **trafic**, pas des identités. L'IP est
|
|
58
|
-
résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts:
|
|
58
|
+
résolue exactement comme pour les logs et l'audit (`resolveForwarded()`, `http-kernel.ts:1173`) —
|
|
59
59
|
non falsifiable tant que `trustProxy` n'accorde pas sa confiance à un proxy.
|
|
60
60
|
2. **Le verdict porte tout.** Un seul appel `hit(key)` (`IRateLimitStore.ts:79`) rend un
|
|
61
61
|
`RateLimitVerdict` (`IRateLimitStore.ts:20`) qui contient déjà limite, restant, reset et
|
|
@@ -106,7 +106,7 @@ Trois choix structurent l'implémentation, et chacun est un compromis assumé.
|
|
|
106
106
|
|
|
107
107
|
**Désactivé par défaut — opt-in explicite.** En cloud-native, le plafond par IP est souvent mieux placé
|
|
108
108
|
à l'**ingress/gateway** (il voit tout le trafic, tous les pods, et rejette avant le coût TLS). Le module
|
|
109
|
-
laisse donc `rateLimit` désarmé par défaut (`config.ts:
|
|
109
|
+
laisse donc `rateLimit` désarmé par défaut (`config.ts:1247`) : `null` tant qu'on ne l'active pas → **0
|
|
110
110
|
coût** sur le chemin chaud. On l'active quand on n'a **pas** d'edge devant soi (bare-metal, VPS), ou en
|
|
111
111
|
défense en profondeur.
|
|
112
112
|
|
|
@@ -118,7 +118,7 @@ court intervalle (`MemoryRateLimitStore.ts:29`). Acceptable pour une défense de
|
|
|
118
118
|
_sliding window_ viendrait en option si le besoin s'en fait sentir.
|
|
119
119
|
|
|
120
120
|
**Refoulé avant toute allocation.** Le verdict est rendu **avant** le contexte, la portée DI et l'ALS
|
|
121
|
-
(`http-kernel.ts:
|
|
121
|
+
(`http-kernel.ts:1172`) : un flood coûte un `Map.get` et rien d'autre. Le contrat `hit()` est
|
|
122
122
|
**synchrone** à dessein (`IRateLimitStore.ts:8`) — aucune `Promise`, aucune microtask sur le chemin de
|
|
123
123
|
chaque requête.
|
|
124
124
|
|
|
@@ -147,7 +147,7 @@ export default defineConfig(() => ({
|
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Les trois clés `enabled` / `windowS` / `max` sont **éditables à chaud** (`runtimeMutable`) : le kernel
|
|
150
|
-
reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts:
|
|
150
|
+
reconstruit le compteur sans redémarrage (`configureRateLimit()`, `http-kernel.ts:497`).
|
|
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:
|
|
182
|
-
- `X-RateLimit-Reset` : **epoch en secondes** de la fin de fenêtre (`http-kernel.ts:
|
|
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`).
|
|
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,26 +203,26 @@ flowchart TD
|
|
|
203
203
|
|
|
204
204
|
Autour de ce cœur, le kernel orchestre le cycle de vie :
|
|
205
205
|
|
|
206
|
-
- **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:
|
|
207
|
-
depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:
|
|
208
|
-
(`http-kernel.ts:
|
|
209
|
-
- **Émission HTTP** : sous le quota, les en-têtes `X-RateLimit-*` sont posés (`http-kernel.ts:
|
|
210
|
-
la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:
|
|
211
|
-
(`http-kernel.ts:
|
|
206
|
+
- **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts: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).
|
|
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
|
|
|
215
215
|
## ⚙️ Configuration
|
|
216
216
|
|
|
217
|
-
Table dérivée de `rateLimitSchema` (`config.ts:
|
|
217
|
+
Table dérivée de `rateLimitSchema` (`config.ts:889`). Tout est optionnel : ce sont les défauts du
|
|
218
218
|
schéma, écrits ici pour les montrer.
|
|
219
219
|
|
|
220
220
|
| Option | Type | Défaut | Effet | Chaud |
|
|
221
221
|
| ------------- | ------------ | --------- | -------------------------------------------------------------------------------- | ----- |
|
|
222
|
-
| `enabled` | bool | `false` | Arme le rate-limit (HTTP **et** handshakes WS, même compteur) (`config.ts:
|
|
223
|
-
| `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts:
|
|
224
|
-
| `max` | int | `300` | Requêtes/IP/fenêtre ; au-delà `429` + `Retry-After` (`config.ts:
|
|
225
|
-
| `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts:
|
|
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
226
|
| `gcIntervalS` | int (s) | `300` | Intervalle du balayage de purge des fenêtres expirées, hors hot-path. | non |
|
|
227
227
|
| `gcJitter` | bool | `true` | Étale le tick GC d'un jitter aléatoire (anti-thundering-herd multi-pod). | non |
|
|
228
228
|
|
|
@@ -230,7 +230,7 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
|
|
|
230
230
|
|
|
231
231
|
| Option | Type | Défaut | Effet | Chaud |
|
|
232
232
|
| ----------------------- | ------------- | ------ | --------------------------------------------------------------------------------------------------- | ----- |
|
|
233
|
-
| `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade fermé en `1013` (`config.ts:
|
|
233
|
+
| `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade fermé en `1013` (`config.ts:1250`). | 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
|
|
@@ -242,19 +242,19 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
|
|
|
242
242
|
Un WebSocket ne peut **pas** recevoir un `429` : au moment où le rate-limit décide, le `101 Switching
|
|
243
243
|
Protocols` est déjà parti sur le fil (émis par la bibliothèque `ws`). Le refoulement se fait donc par
|
|
244
244
|
une **fermeture RFC 6455 `1013 Try Again Later`**, décidée dans `onWebsocketRequest()`
|
|
245
|
-
(`http-kernel.ts:
|
|
245
|
+
(`http-kernel.ts:1964`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
|
|
246
246
|
|
|
247
247
|
Deux plafonds distincts, tous deux par IP forwarded-aware :
|
|
248
248
|
|
|
249
249
|
| Plafond | Ce qu'il borne | Source de config | Refus |
|
|
250
250
|
| ---------------------- | ---------------------------------------------- | ----------------------- | ---------------------------------------------------------- |
|
|
251
|
-
| Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:
|
|
252
|
-
| Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:
|
|
251
|
+
| Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:471`) |
|
|
252
|
+
| Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:2240`) |
|
|
253
253
|
|
|
254
254
|
Le cap concurrent est porté par un compteur dédié, `WsConnectionCounter` (`WsConnectionCounter.ts:18`) :
|
|
255
255
|
`tryAcquire(ip)` (`WsConnectionCounter.ts:33`) réserve un créneau à l'upgrade, `release(ip)`
|
|
256
256
|
(`WsConnectionCounter.ts:45`) le rend à la fermeture — branché sur `ws.once("close", …)`
|
|
257
|
-
(`http-kernel.ts:
|
|
257
|
+
(`http-kernel.ts:1918`), donc jamais de fuite de compteur, même sur un `terminate` de heartbeat.
|
|
258
258
|
|
|
259
259
|
> [!WARNING]
|
|
260
260
|
> `wsMaxConnectionsPerIp` a une **portée par process** (1 pod) : il ne voit que le trafic de son propre
|
|
@@ -280,14 +280,14 @@ Un adapter doit fournir : `hit(key)` (verdict de fenêtre), `gc()` (purge), `lis
|
|
|
280
280
|
|
|
281
281
|
| Domaine | Norme | Ancrage |
|
|
282
282
|
| ---------------------------------- | ---------------- | ------------------------------------------------- |
|
|
283
|
-
| `429 Too Many Requests` | RFC 6585 §4 | `writeHead(429)` (`http-kernel.ts:
|
|
284
|
-
| `Retry-After` (delta-seconds) | RFC 9110 §10.2.3 | en-tête posé sur le `429` (`http-kernel.ts:
|
|
285
|
-
| IP cliente derrière proxy | RFC 7239 | `resolveForwarded()` (`http-kernel.ts:
|
|
286
|
-
| WebSocket — close `1013` Try Again | RFC 6455 §7.4.1 | refus d'upgrade (`http-kernel.ts:
|
|
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`) |
|
|
287
287
|
|
|
288
288
|
> [!NOTE]
|
|
289
289
|
> Les en-têtes émis sont la famille **de facto** `X-RateLimit-Limit/Remaining/Reset`
|
|
290
|
-
> (`http-kernel.ts:
|
|
290
|
+
> (`http-kernel.ts:1129`), 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
|
|
|
@@ -302,8 +302,8 @@ Les choix visibles dans le code :
|
|
|
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:
|
|
306
|
-
- **Résolution d'IP seulement si un limiteur est armé** côté WS (`http-kernel.ts:
|
|
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`).
|
|
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
|
|
@@ -329,7 +329,7 @@ Le data plane admin expose **qui martèle** via `createHttpAdminApi()` (`HttpAdm
|
|
|
329
329
|
| Le rate-limit « ne fait rien » | Désactivé par défaut (opt-in) | `rateLimit.enabled: true` dans `use("@nodefony/http", …)` |
|
|
330
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
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:
|
|
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
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
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
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) |
|