@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.
Files changed (84) hide show
  1. package/README.md +5 -3
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +11 -5
  6. package/dist/nodefony/command/certificatesCommand.js +0 -1
  7. package/dist/nodefony/config/config.js +48 -7
  8. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  9. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  10. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  11. package/dist/nodefony/service/audit-logger.js +2 -3
  12. package/dist/nodefony/service/certificates.js +214 -158
  13. package/dist/nodefony/service/error-renderer.js +1 -1
  14. package/dist/nodefony/service/http-kernel.js +221 -47
  15. package/dist/nodefony/service/reverse-proxy.js +336 -0
  16. package/dist/nodefony/service/servers/server-http.js +3 -3
  17. package/dist/nodefony/service/servers/server-https.js +3 -3
  18. package/dist/nodefony/service/servers/server-static.js +4 -7
  19. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  20. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  21. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  22. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  23. package/dist/nodefony/service/trace.js +2 -1
  24. package/dist/nodefony/service/upload/upload-service.js +3 -3
  25. package/dist/nodefony/service/x509.js +324 -0
  26. package/dist/nodefony/src/context/Context.js +28 -1
  27. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  28. package/dist/nodefony/src/context/forwarded.js +2 -2
  29. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  30. package/dist/nodefony/src/context/http/Request.js +9 -4
  31. package/dist/nodefony/src/context/http/Response.js +46 -7
  32. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
  34. package/dist/nodefony/src/context/responseEnded.js +27 -0
  35. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  36. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  37. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  38. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  39. package/dist/nodefony/src/proxy/forward.js +476 -0
  40. package/dist/nodefony/src/proxy/rules.js +182 -0
  41. package/dist/nodefony/src/servers/portBinder.js +57 -2
  42. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  43. package/dist/nodefony/src/session/session.js +28 -12
  44. package/dist/types/index.d.ts +5 -0
  45. package/dist/types/nodefony/config/config.d.ts +17 -0
  46. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  47. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  48. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  49. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  50. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  52. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  53. package/dist/types/nodefony/service/certificates.d.ts +126 -50
  54. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  55. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  56. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  57. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  58. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  59. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  60. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  61. package/dist/types/nodefony/service/x509.d.ts +86 -0
  62. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  63. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  64. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  65. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  66. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  67. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  68. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  69. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  70. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  71. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  72. package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
  73. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  74. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  75. package/docs/cookies.md +13 -13
  76. package/docs/index.md +9 -3
  77. package/docs/observabilite.md +18 -18
  78. package/docs/rate-limit.md +30 -30
  79. package/docs/reverse-proxy.md +522 -0
  80. package/docs/servers.md +115 -78
  81. package/docs/session.md +61 -61
  82. package/docs/sse.md +283 -0
  83. package/docs/upload.md +37 -37
  84. package/package.json +10 -13
@@ -4,6 +4,8 @@ import { resolveForwarded } from "../src/context/forwarded.js";
4
4
  import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
5
5
  import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
6
6
  import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
7
+ import { responseEnded } from "../src/context/responseEnded.js";
8
+ import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
7
9
  import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
8
10
  import HttpContext from "../src/context/http/HttpContext.js";
9
11
  import DefaultErrorRenderer from "./error-renderer.js";
@@ -12,9 +14,9 @@ import JsonAuditLogger from "./audit-logger.js";
12
14
  import PrettyRequestLogger from "./pretty-request-logger.js";
13
15
  import { resolveTraceparent } from "./trace.js";
14
16
  import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
15
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
16
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
17
- import { GcScheduler, Module, RequestContext, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
17
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
18
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
19
+ import { GcScheduler, IDENTITY_CHANGED_EVENT, Module, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
18
20
  import cluster from "node:cluster";
19
21
  import http from "node:http";
20
22
  import Ws from "ws";
@@ -67,13 +69,37 @@ let HttpKernel = class HttpKernel extends Service {
67
69
  cert = "";
68
70
  ca = "";
69
71
  serverStatic = null;
72
+ /**
73
+ * Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
74
+ * `mounts === null` tant que personne n'a monté de préfixe.
75
+ */
76
+ reverseProxy = null;
70
77
  domain = "";
71
78
  trustedHosts;
72
- domainCheck = false;
79
+ /**
80
+ * Contrôle de domaine actif (`domainCheck` de la config d'application) —
81
+ * lu à la source, pour le HTTP comme pour le handshake WebSocket.
82
+ */
83
+ get domainCheck() {
84
+ return this.kernel?.options.domainCheck === true;
85
+ }
73
86
  regAlias = [];
74
87
  module;
75
- httpsPort;
76
- httpPort;
88
+ /**
89
+ * Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
90
+ * lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
91
+ */
92
+ get httpsPort() {
93
+ return this.boundPort("server-https");
94
+ }
95
+ /** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
96
+ get httpPort() {
97
+ return this.boundPort("server-http");
98
+ }
99
+ boundPort(name) {
100
+ const server = this.get(name);
101
+ return server?.active === true && server.port > 0 ? server.port : void 0;
102
+ }
77
103
  responseTimeout;
78
104
  closeTimeOutWs;
79
105
  secContentTypeOptions = null;
@@ -90,6 +116,7 @@ let HttpKernel = class HttpKernel extends Service {
90
116
  rateLimiter = null;
91
117
  rateLimitGc = null;
92
118
  #wsConnCounter = null;
119
+ #streams = null;
93
120
  #healthPaths = null;
94
121
  #healthShutdownDelay = 0;
95
122
  #terminating = false;
@@ -108,6 +135,9 @@ let HttpKernel = class HttpKernel extends Service {
108
135
  wss: this.options.websocketSecure.closeTimeout
109
136
  };
110
137
  this.computeSecurityHeaderCaches();
138
+ this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
139
+ this.#streams?.refreshIdentity(event.identifier);
140
+ });
111
141
  }
112
142
  /**
113
143
  * (Re)calcule les caches dérivés des en-têtes de sécurité depuis `this.options`
@@ -244,6 +274,7 @@ let HttpKernel = class HttpKernel extends Service {
244
274
  this.kernel?.prependOnceListener("onReady", () => {
245
275
  this.serviceCerticats = this.get("certificates");
246
276
  this.serverStatic = this.get("server-static");
277
+ this.reverseProxy = this.get("reverse-proxy");
247
278
  this.domain = this.kernel?.domain;
248
279
  this.trustedHosts = this.options.trustedHosts;
249
280
  this.regAlias = this.compileAlias();
@@ -307,6 +338,35 @@ let HttpKernel = class HttpKernel extends Service {
307
338
  return policy;
308
339
  }
309
340
  /**
341
+ * L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
342
+ * interrogeable SANS contexte : le serveur WebSocket de Nodefony
343
+ * ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
344
+ * avant qu'aucun contexte n'existe, décident par elle.
345
+ *
346
+ * Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
347
+ * en development, allowlist `allowedOrigins` ; sans `Origin` (client
348
+ * non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
349
+ *
350
+ * @param origin - valeur brute de l'en-tête `Origin`
351
+ * @param hostname - nom d'hôte servi, sans port (`Context.domain`)
352
+ * @param secure - handshake reçu en TLS (politique `websocketSecure`)
353
+ * @returns `true` si le handshake peut se poursuivre
354
+ */
355
+ isWebsocketOriginAllowed(origin, hostname, secure) {
356
+ const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
357
+ if (policy.disabled) return true;
358
+ if (!origin) return true;
359
+ let originHost = null;
360
+ try {
361
+ originHost = new URL(origin).hostname;
362
+ } catch {
363
+ return false;
364
+ }
365
+ if (originHost === hostname) return true;
366
+ if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return true;
367
+ return policy.extra.length > 0 && isDomainAllowed(policy.extra, originHost);
368
+ }
369
+ /**
310
370
  * B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
311
371
  * WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
312
372
  * ce contrôle, une page tierce peut ouvrir un WS **authentifié par le cookie de
@@ -322,22 +382,9 @@ let HttpKernel = class HttpKernel extends Service {
322
382
  * @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
323
383
  */
324
384
  checkWebsocketOrigin(context) {
325
- const cfgKey = context.type === "websocket-secure" ? "websocketSecure" : "websocket";
326
- const policy = this.getWsOriginPolicy(cfgKey);
327
- if (policy.disabled) return;
385
+ const secure = context.type === "websocket-secure";
328
386
  const raw = context.origin;
329
- if (!raw) return;
330
- let originHost = null;
331
- try {
332
- originHost = new URL(raw).hostname;
333
- } catch {
334
- originHost = null;
335
- }
336
- if (originHost) {
337
- if (originHost === context.domain) return;
338
- if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return;
339
- if (policy.extra.length && isDomainAllowed(policy.extra, originHost)) return;
340
- }
387
+ if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
341
388
  const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
342
389
  error.type = "origin";
343
390
  throw error;
@@ -483,7 +530,7 @@ let HttpKernel = class HttpKernel extends Service {
483
530
  */
484
531
  /**
485
532
  * Override the default error renderer — e.g. to hide stack traces in prod
486
- * or emit RFC 7807 problem+json. Stateless singleton expected.
533
+ * or emit RFC 9457 problem+json. Stateless singleton expected.
487
534
  */
488
535
  setErrorRenderer(renderer) {
489
536
  this.errorRenderer = renderer;
@@ -504,6 +551,10 @@ let HttpKernel = class HttpKernel extends Service {
504
551
  async onError(error, context, _extraHeaders) {
505
552
  if (context) context.error = error;
506
553
  if (context instanceof HttpContext) {
554
+ if (context.isRedirect && error.code === 401) {
555
+ if (!(context.finished || context.sended)) await context.send();
556
+ return context;
557
+ }
507
558
  const result = this.errorRenderer.renderHttp(error, context);
508
559
  if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
509
560
  context.response.setStatusCode(result.status, http.STATUS_CODES[result.status] ?? "Unknown Error");
@@ -514,6 +565,10 @@ let HttpKernel = class HttpKernel extends Service {
514
565
  return context;
515
566
  }
516
567
  if (!context.response.isHeaderSent()) {
568
+ if (context.isRedirect) {
569
+ context.isRedirect = false;
570
+ context.response.response?.removeHeader("Location");
571
+ }
517
572
  try {
518
573
  await context.render(result.body);
519
574
  } catch (e) {
@@ -799,7 +854,7 @@ let HttpKernel = class HttpKernel extends Service {
799
854
  perfProbe.t0 = 0n;
800
855
  } else context = new HttpContext(scope, request, response, type);
801
856
  response.on("close", () => {
802
- if (!response.writableEnded) {
857
+ if (!responseEnded(response)) {
803
858
  context._abortIfPending("Connection closed before response finished");
804
859
  if (!context.sended) context.response.statusCode = 499;
805
860
  }
@@ -842,14 +897,16 @@ let HttpKernel = class HttpKernel extends Service {
842
897
  const profilerQueries = this.profiler ? [] : null;
843
898
  context.profilerQueries = profilerQueries;
844
899
  context.profiling = profilerQueries !== null;
845
- return RequestContext.run({
900
+ const store = {
846
901
  requestId: context.requestId,
847
902
  scheme: context.scheme,
848
903
  traceparent: context.traceparent,
849
904
  queries: profilerQueries ?? void 0,
850
905
  context,
851
906
  scope
852
- }, () => this.runHttpPipeline(context, request, response));
907
+ };
908
+ context.holdRequestStore(store);
909
+ return RequestContext.run(store, () => this.runHttpPipeline(context, request, response));
853
910
  }
854
911
  /**
855
912
  * Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
@@ -887,6 +944,10 @@ let HttpKernel = class HttpKernel extends Service {
887
944
  * N'attend que le repli statique, et seulement quand aucune route ne matche.
888
945
  */
889
946
  routeHttpRequest(httpContext, request, response) {
947
+ if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
948
+ const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
949
+ if (relayed !== void 0) return relayed.then(() => httpContext);
950
+ }
890
951
  httpContext.phaseStart("resolve");
891
952
  httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
892
953
  httpContext.phaseEnd("resolve");
@@ -931,7 +992,7 @@ let HttpKernel = class HttpKernel extends Service {
931
992
  if (error) throw error;
932
993
  const schemeHeaders = context.scheme === "https" ? this.options.https.headers : context.scheme === "http" ? this.options.http.headers : null;
933
994
  if (schemeHeaders) context.response.setHeaders(schemeHeaders);
934
- if (this.kernel?.options.domainCheck) this.checkValidDomain(context);
995
+ if (this.domainCheck) this.checkValidDomain(context);
935
996
  if (this.listenerCount("beforeResolve")) return this.fireAsync("beforeResolve", context).then(() => this.armAndGuard(context));
936
997
  return this.armAndGuard(context);
937
998
  }
@@ -942,7 +1003,7 @@ let HttpKernel = class HttpKernel extends Service {
942
1003
  armAndGuard(context) {
943
1004
  this.prepareFrontController(context);
944
1005
  this.firewall?.enforceCsrf(context);
945
- return thenMaybe(this.startSession(context), () => context.secure || context.isControlledAccess ? this.runFirewall(context) : context);
1006
+ return thenMaybe(this.startSession(context), () => context.secure ? this.runFirewall(context) : context);
946
1007
  }
947
1008
  /**
948
1009
  * Le pare-feu d'une requête en zone protégée : `handleSecurity`, puis les
@@ -997,22 +1058,10 @@ let HttpKernel = class HttpKernel extends Service {
997
1058
  }
998
1059
  }
999
1060
  async onWebsocketRequest(ws, req, type) {
1000
- const wsConnCounter = this.#wsConnCounter;
1001
- if (this.rateLimiter !== null || wsConnCounter !== null) {
1002
- const ip = resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
1003
- if (ip !== null) {
1004
- if (this.rateLimiter?.hit(ip).limited) {
1005
- if (ws.readyState === Ws.OPEN) ws.close(1013, "rate limit");
1006
- return;
1007
- }
1008
- if (wsConnCounter !== null) {
1009
- if (!wsConnCounter.tryAcquire(ip)) {
1010
- if (ws.readyState === Ws.OPEN) ws.close(1013, "too many connections");
1011
- return;
1012
- }
1013
- ws.once("close", () => wsConnCounter.release(ip));
1014
- }
1015
- }
1061
+ const refusal = this.websocketQuotaRefusal(req, ws);
1062
+ if (refusal !== null) {
1063
+ if (ws.readyState === Ws.OPEN) ws.close(1013, refusal);
1064
+ return;
1016
1065
  }
1017
1066
  await this.fireAsync("onServerRequest", req, null, type);
1018
1067
  const scope = this.container?.enterScope("request");
@@ -1034,15 +1083,17 @@ let HttpKernel = class HttpKernel extends Service {
1034
1083
  const wsRunId = context?.requestId ?? "ws-no-ctx";
1035
1084
  const wsScheme = context?.scheme ?? "ws";
1036
1085
  const wsTrace = context?.traceparent ?? null;
1037
- return await RequestContext.run({
1086
+ const store = {
1038
1087
  requestId: wsRunId,
1039
1088
  scheme: wsScheme,
1040
1089
  ...wsTrace ? { traceparent: wsTrace } : {},
1041
1090
  context,
1042
1091
  scope
1043
- }, async () => {
1092
+ };
1093
+ context?.holdRequestStore(store);
1094
+ return await RequestContext.run(store, async () => {
1044
1095
  await this.onConnect(context, error);
1045
- if (this.firewall && (context?.secure || context?.isControlledAccess)) {
1096
+ if (this.firewall && context?.secure) {
1046
1097
  context.phaseStart("firewall");
1047
1098
  try {
1048
1099
  try {
@@ -1126,8 +1177,131 @@ let HttpKernel = class HttpKernel extends Service {
1126
1177
  isTrustedHostname(hostname) {
1127
1178
  return isDomainAllowed(this.regAlias, hostname);
1128
1179
  }
1180
+ /**
1181
+ * Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
1182
+ * (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
1183
+ * inverse et avant le serveur `ws`.
1184
+ *
1185
+ * Pourquoi avant : accepter puis fermer fait croire au client que le serveur
1186
+ * a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
1187
+ * une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
1188
+ * recharger la page avant que Vite soit prêt — page blanche.
1189
+ *
1190
+ * Ne juge que l'existence d'une route WebSocket pour le chemin
1191
+ * ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
1192
+ * quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
1193
+ * compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
1194
+ * flot d'upgrades vers des chemins inexistants y échapperait.
1195
+ *
1196
+ * @param req - requête d'upgrade
1197
+ * @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
1198
+ * inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
1199
+ * ou `null` si l'upgrade est admis
1200
+ */
1201
+ websocketUpgradeRefusal(req) {
1202
+ const router = this.router;
1203
+ if (router === null || router === void 0) return 503;
1204
+ let pathname;
1205
+ try {
1206
+ pathname = parseWebsocketUrl("ws", req.headers.host, req.url).pathname;
1207
+ } catch {
1208
+ return this.#rateLimitedRefusal(req) ?? 400;
1209
+ }
1210
+ if (router.servesWebsocket(pathname)) return null;
1211
+ return this.#rateLimitedRefusal(req) ?? 404;
1212
+ }
1213
+ /** Compte un upgrade refusé au limiteur de débit : `429` s'il le dépasse. */
1214
+ #rateLimitedRefusal(req) {
1215
+ if (this.rateLimiter === null) return null;
1216
+ const ip = this.#clientIp(req);
1217
+ return ip !== null && this.rateLimiter.hit(ip).limited ? 429 : null;
1218
+ }
1219
+ /**
1220
+ * Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
1221
+ * compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
1222
+ * simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
1223
+ * le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
1224
+ * qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
1225
+ * servie.
1226
+ *
1227
+ * L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
1228
+ * un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
1229
+ * par refus : sous un flood, il serait lui-même un amplificateur.
1230
+ *
1231
+ * @param req - requête d'upgrade
1232
+ * @param connection - ce dont la fermeture libère la place comptée (le `ws`
1233
+ * accepté, ou le socket du client pour un tunnel relayé)
1234
+ * @returns `null` si l'upgrade est admis — sa place est alors rendue à la
1235
+ * fermeture de `connection` —, sinon la raison du refus
1236
+ */
1237
+ websocketQuotaRefusal(req, connection) {
1238
+ if (this.rateLimiter === null && this.#wsConnCounter === null) return null;
1239
+ const ip = this.#clientIp(req);
1240
+ if (ip === null) return null;
1241
+ if (this.rateLimiter?.hit(ip).limited) return "rate limit";
1242
+ return this.#acquireSlot(ip, connection) ? null : "too many connections";
1243
+ }
1244
+ /**
1245
+ * Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
1246
+ * `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
1247
+ * un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
1248
+ * Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
1249
+ * à l'entrée du pipeline.
1250
+ *
1251
+ * @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
1252
+ * @param connection - ce dont la fermeture rend la place (la réponse)
1253
+ * @returns `true` si la place est prise — ou si aucun plafond n'est armé
1254
+ */
1255
+ acquireConnectionSlot(req, connection) {
1256
+ if (this.#wsConnCounter === null) return true;
1257
+ const ip = this.#clientIp(req);
1258
+ return ip === null || this.#acquireSlot(ip, connection);
1259
+ }
1260
+ /** IP cliente, résolue comme en HTTP (RFC 7239 + `trustProxy`). */
1261
+ #clientIp(req) {
1262
+ return resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
1263
+ }
1264
+ /**
1265
+ * Compte une connexion de `ip` ; sa place est rendue à la fermeture de
1266
+ * `connection`. Le compteur est capturé : la connexion décrémente TOUJOURS
1267
+ * l'instance qui l'a comptée, même après reconfiguration — et `once("close")`
1268
+ * part à toute fermeture (`terminate` du battement, rejet du pare-feu…).
1269
+ */
1270
+ #acquireSlot(ip, connection) {
1271
+ const counter = this.#wsConnCounter;
1272
+ if (counter === null) return true;
1273
+ if (!counter.tryAcquire(ip)) return false;
1274
+ connection.once("close", () => counter.release(ip));
1275
+ return true;
1276
+ }
1277
+ /**
1278
+ * Inscrit un flux SSE à identité révocable : il sera fermé dans la fenêtre de
1279
+ * re-validation qui suit la mort de son identité (déconnexion, session
1280
+ * révoquée, jeton expiré ou annulé) — le même registre et la même règle que
1281
+ * les sockets WebSocket.
1282
+ *
1283
+ * @internal — appelé par le flux lui-même.
1284
+ */
1285
+ watchStream(stream) {
1286
+ (this.#streams ??= new RevocationWatch({
1287
+ isValid: (entry) => entry.revalidate(),
1288
+ revoke: (entry) => void entry.close(),
1289
+ identityOf: (entry) => entry.identifier ?? void 0
1290
+ })).register(stream);
1291
+ }
1292
+ /** Retire un flux du registre de révocation (à sa fermeture). @internal */
1293
+ unwatchStream(stream) {
1294
+ this.#streams?.unregister(stream);
1295
+ }
1296
+ /**
1297
+ * Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
1298
+ * Publique pour qu'un banc la déclenche sans attendre le tick.
1299
+ */
1300
+ revalidateStreams() {
1301
+ return this.#streams?.revalidate() ?? Promise.resolve();
1302
+ }
1129
1303
  };
1130
1304
  HttpKernel = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], HttpKernel);
1131
1305
  var http_kernel_default = HttpKernel;
1132
1306
  //#endregion
1133
- export { http_kernel_default as default };
1307
+ export { http_kernel_default as default, responseEnded };