@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4

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 (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  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 +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  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 +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  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 +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  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 +22 -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/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -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 +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -4,21 +4,22 @@ 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 WebsocketContext from "../src/context/websocket/WebsocketContext.js";
7
+ import { responseEnded } from "../src/context/responseEnded.js";
8
+ import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
9
+ import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
10
+ import { trackPong } from "./servers/wsHeartbeat.js";
8
11
  import HttpContext from "../src/context/http/HttpContext.js";
9
12
  import DefaultErrorRenderer from "./error-renderer.js";
10
- import DefaultRequestLogger from "./request-logger.js";
11
13
  import JsonAuditLogger from "./audit-logger.js";
14
+ import DefaultRequestLogger from "./request-logger.js";
12
15
  import PrettyRequestLogger from "./pretty-request-logger.js";
13
16
  import { resolveTraceparent } from "./trace.js";
14
17
  import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
15
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
16
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
17
- import { GcScheduler, Module, RequestContext, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
18
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
19
+ import { GcScheduler, IDENTITY_CHANGED_EVENT, 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
- import Ws from "ws";
21
- //#region nodefony/service/http-kernel.ts
22
+ // nodefony/service/http-kernel.ts
22
23
  const HEALTH_HEADERS = {
23
24
  "content-type": "application/json",
24
25
  "cache-control": "no-store"
@@ -67,13 +68,37 @@ let HttpKernel = class HttpKernel extends Service {
67
68
  cert = "";
68
69
  ca = "";
69
70
  serverStatic = null;
71
+ /**
72
+ * Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
73
+ * `mounts === null` tant que personne n'a monté de préfixe.
74
+ */
75
+ reverseProxy = null;
70
76
  domain = "";
71
77
  trustedHosts;
72
- domainCheck = false;
78
+ /**
79
+ * Contrôle de domaine actif (`domainCheck` de la config d'application) —
80
+ * lu à la source, pour le HTTP comme pour le handshake WebSocket.
81
+ */
82
+ get domainCheck() {
83
+ return this.kernel?.options.domainCheck === true;
84
+ }
73
85
  regAlias = [];
74
86
  module;
75
- httpsPort;
76
- httpPort;
87
+ /**
88
+ * Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
89
+ * lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
90
+ */
91
+ get httpsPort() {
92
+ return this.boundPort("server-https");
93
+ }
94
+ /** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
95
+ get httpPort() {
96
+ return this.boundPort("server-http");
97
+ }
98
+ boundPort(name) {
99
+ const server = this.get(name);
100
+ return server?.active === true && server.port > 0 ? server.port : void 0;
101
+ }
77
102
  responseTimeout;
78
103
  closeTimeOutWs;
79
104
  secContentTypeOptions = null;
@@ -90,6 +115,7 @@ let HttpKernel = class HttpKernel extends Service {
90
115
  rateLimiter = null;
91
116
  rateLimitGc = null;
92
117
  #wsConnCounter = null;
118
+ #streams = null;
93
119
  #healthPaths = null;
94
120
  #healthShutdownDelay = 0;
95
121
  #terminating = false;
@@ -99,15 +125,16 @@ let HttpKernel = class HttpKernel extends Service {
99
125
  this.container?.addScope("request");
100
126
  this.responseTimeout = {
101
127
  http: this.options.http.responseTimeout,
102
- https: this.options.https.responseTimeout,
103
- http2: this.options.https.responseTimeout,
104
- http3: this.options.https.responseTimeout
128
+ https: this.options.https.responseTimeout
105
129
  };
106
130
  this.closeTimeOutWs = {
107
131
  ws: this.options.websocket.closeTimeout,
108
132
  wss: this.options.websocketSecure.closeTimeout
109
133
  };
110
134
  this.computeSecurityHeaderCaches();
135
+ this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
136
+ this.#streams?.refreshIdentity(event.identifier);
137
+ });
111
138
  }
112
139
  /**
113
140
  * (Re)calcule les caches dérivés des en-têtes de sécurité depuis `this.options`
@@ -244,6 +271,7 @@ let HttpKernel = class HttpKernel extends Service {
244
271
  this.kernel?.prependOnceListener("onReady", () => {
245
272
  this.serviceCerticats = this.get("certificates");
246
273
  this.serverStatic = this.get("server-static");
274
+ this.reverseProxy = this.get("reverse-proxy");
247
275
  this.domain = this.kernel?.domain;
248
276
  this.trustedHosts = this.options.trustedHosts;
249
277
  this.regAlias = this.compileAlias();
@@ -307,6 +335,35 @@ let HttpKernel = class HttpKernel extends Service {
307
335
  return policy;
308
336
  }
309
337
  /**
338
+ * L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
339
+ * interrogeable SANS contexte : le serveur WebSocket de Nodefony
340
+ * ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
341
+ * avant qu'aucun contexte n'existe, décident par elle.
342
+ *
343
+ * Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
344
+ * en development, allowlist `allowedOrigins` ; sans `Origin` (client
345
+ * non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
346
+ *
347
+ * @param origin - valeur brute de l'en-tête `Origin`
348
+ * @param hostname - nom d'hôte servi, sans port (`Context.domain`)
349
+ * @param secure - handshake reçu en TLS (politique `websocketSecure`)
350
+ * @returns `true` si le handshake peut se poursuivre
351
+ */
352
+ isWebsocketOriginAllowed(origin, hostname, secure) {
353
+ const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
354
+ if (policy.disabled) return true;
355
+ if (!origin) return true;
356
+ let originHost;
357
+ try {
358
+ originHost = new URL(origin).hostname;
359
+ } catch {
360
+ return false;
361
+ }
362
+ if (originHost === hostname) return true;
363
+ if (this.kernel?.environment === "development" && WS_DEV_LOOPBACK.has(originHost)) return true;
364
+ return policy.extra.length > 0 && isDomainAllowed(policy.extra, originHost);
365
+ }
366
+ /**
310
367
  * B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
311
368
  * WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
312
369
  * ce contrôle, une page tierce peut ouvrir un WS **authentifié par le cookie de
@@ -322,22 +379,9 @@ let HttpKernel = class HttpKernel extends Service {
322
379
  * @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
323
380
  */
324
381
  checkWebsocketOrigin(context) {
325
- const cfgKey = context.type === "websocket-secure" ? "websocketSecure" : "websocket";
326
- const policy = this.getWsOriginPolicy(cfgKey);
327
- if (policy.disabled) return;
382
+ const secure = context.serverType === "wss";
328
383
  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
- }
384
+ if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
341
385
  const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
342
386
  error.type = "origin";
343
387
  throw error;
@@ -434,7 +478,14 @@ let HttpKernel = class HttpKernel extends Service {
434
478
  context.resolver = resolver;
435
479
  return resolver;
436
480
  }
437
- if (resolver.exception) throw resolver.exception;
481
+ if (resolver.exception) {
482
+ const exception = resolver.exception;
483
+ if (context.secure && "code" in exception && exception.code === 426) {
484
+ context.resolver = resolver;
485
+ return resolver;
486
+ }
487
+ throw resolver.exception;
488
+ }
438
489
  throw new HttpError("Not Found", 404, context);
439
490
  }
440
491
  /**
@@ -483,7 +534,7 @@ let HttpKernel = class HttpKernel extends Service {
483
534
  */
484
535
  /**
485
536
  * Override the default error renderer — e.g. to hide stack traces in prod
486
- * or emit RFC 7807 problem+json. Stateless singleton expected.
537
+ * or emit RFC 9457 problem+json. Stateless singleton expected.
487
538
  */
488
539
  setErrorRenderer(renderer) {
489
540
  this.errorRenderer = renderer;
@@ -504,6 +555,10 @@ let HttpKernel = class HttpKernel extends Service {
504
555
  async onError(error, context, _extraHeaders) {
505
556
  if (context) context.error = error;
506
557
  if (context instanceof HttpContext) {
558
+ if (context.isRedirect && error.code === 401) {
559
+ if (!(context.finished || context.sended)) await context.send();
560
+ return context;
561
+ }
507
562
  const result = this.errorRenderer.renderHttp(error, context);
508
563
  if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
509
564
  context.response.setStatusCode(result.status, http.STATUS_CODES[result.status] ?? "Unknown Error");
@@ -514,6 +569,10 @@ let HttpKernel = class HttpKernel extends Service {
514
569
  return context;
515
570
  }
516
571
  if (!context.response.isHeaderSent()) {
572
+ if (context.isRedirect) {
573
+ context.isRedirect = false;
574
+ context.response.response?.removeHeader("Location");
575
+ }
517
576
  try {
518
577
  await context.render(result.body);
519
578
  } catch (e) {
@@ -525,18 +584,26 @@ let HttpKernel = class HttpKernel extends Service {
525
584
  await context.close();
526
585
  return context;
527
586
  }
528
- if (context instanceof WebsocketContext) try {
529
- const wsResult = this.errorRenderer.renderWebsocket(error, context);
530
- if (context.response?.connection) {
531
- context.close(wsResult.code, wsResult.reason);
587
+ if (context instanceof WebsocketContext) {
588
+ if (context.admission !== null) {
589
+ const status = toHandshakeStatus("code" in error && typeof error.code === "number" ? error.code : void 0);
590
+ context.refuseHandshake(status);
591
+ if (this.kernel?.debug) this.log(`${status} ${error.message}`, "DEBUG", "WEBSOCKET REFUSED");
532
592
  return context;
533
593
  }
534
- if (context.request && !context.rejected) {
535
- context.reject(wsResult.code, wsResult.reason);
536
- return context;
594
+ try {
595
+ const wsResult = this.errorRenderer.renderWebsocket(error, context);
596
+ if (context.response?.connection) {
597
+ context.close(wsResult.code, wsResult.reason);
598
+ return context;
599
+ }
600
+ if (context.request && !context.rejected) {
601
+ context.reject(wsResult.code, wsResult.reason);
602
+ return context;
603
+ }
604
+ } catch {
605
+ throw error;
537
606
  }
538
- } catch {
539
- throw error;
540
607
  }
541
608
  throw error;
542
609
  }
@@ -578,7 +645,7 @@ let HttpKernel = class HttpKernel extends Service {
578
645
  if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
579
646
  if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
580
647
  if (this.secFrameOptions !== null) response.setHeader("X-Frame-Options", this.secFrameOptions);
581
- if (this.secHsts !== null && (type === "https" || type === "http2")) response.setHeader("Strict-Transport-Security", this.secHsts);
648
+ if (this.secHsts !== null && type === "https") response.setHeader("Strict-Transport-Security", this.secHsts);
582
649
  if (this.#healthPaths !== null) {
583
650
  const url = request.url;
584
651
  if (url === this.#healthPaths.liveness) {
@@ -621,6 +688,11 @@ let HttpKernel = class HttpKernel extends Service {
621
688
  await serverHttps.createServer();
622
689
  servers.push(serverHttps);
623
690
  }
691
+ const serverH2c = this.get("server-h2c");
692
+ if (serverH2c?.active) {
693
+ await serverH2c.createServer();
694
+ servers.push(serverH2c);
695
+ }
624
696
  const serverWebsocket = this.get("server-websocket");
625
697
  if (serverWebsocket && serverHttp?.active) {
626
698
  await serverWebsocket.createServer(serverHttp);
@@ -631,7 +703,7 @@ let HttpKernel = class HttpKernel extends Service {
631
703
  await serverWebsocketSecure.createServer(serverHttps);
632
704
  servers.push(serverWebsocketSecure);
633
705
  }
634
- this.publishRuntimePorts(serverHttp, serverHttps);
706
+ this.publishRuntimePorts(serverHttp, serverHttps, serverH2c);
635
707
  return servers;
636
708
  }
637
709
  /**
@@ -665,7 +737,7 @@ let HttpKernel = class HttpKernel extends Service {
665
737
  * {@link readRuntimeState} — `status`/`stop` redeviendraient aveugles alors que
666
738
  * l'app écoute toujours. Le master, lui, est le propriétaire du runtime.
667
739
  */
668
- publishRuntimePorts(serverHttp, serverHttps) {
740
+ publishRuntimePorts(serverHttp, serverHttps, serverH2c) {
669
741
  const ports = [];
670
742
  const desired = [];
671
743
  const urls = [];
@@ -681,6 +753,11 @@ let HttpKernel = class HttpKernel extends Service {
681
753
  const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "https");
682
754
  if (port) desired.push(port);
683
755
  }
756
+ if (serverH2c?.active && serverH2c.port > 0) {
757
+ ports.push(serverH2c.port);
758
+ const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "h2c");
759
+ if (port) desired.push(port);
760
+ }
684
761
  if (ports.length === 0) return;
685
762
  writeRuntimeState(process.cwd(), {
686
763
  pid: cluster.isWorker ? process.ppid : process.pid,
@@ -799,9 +876,9 @@ let HttpKernel = class HttpKernel extends Service {
799
876
  perfProbe.t0 = 0n;
800
877
  } else context = new HttpContext(scope, request, response, type);
801
878
  response.on("close", () => {
802
- if (!response.writableEnded) {
879
+ if (!responseEnded(response)) {
803
880
  context._abortIfPending("Connection closed before response finished");
804
- if (!context.sended) context.response.statusCode = 499;
881
+ if (context.response.statusCode < 400) context.response.statusCode = 499;
805
882
  }
806
883
  this.teardownHttp(context, scope);
807
884
  });
@@ -842,14 +919,16 @@ let HttpKernel = class HttpKernel extends Service {
842
919
  const profilerQueries = this.profiler ? [] : null;
843
920
  context.profilerQueries = profilerQueries;
844
921
  context.profiling = profilerQueries !== null;
845
- return RequestContext.run({
922
+ const store = {
846
923
  requestId: context.requestId,
847
924
  scheme: context.scheme,
848
925
  traceparent: context.traceparent,
849
926
  queries: profilerQueries ?? void 0,
850
927
  context,
851
928
  scope
852
- }, () => this.runHttpPipeline(context, request, response));
929
+ };
930
+ context.holdRequestStore(store);
931
+ return RequestContext.run(store, () => this.runHttpPipeline(context, request, response));
853
932
  }
854
933
  /**
855
934
  * Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
@@ -887,6 +966,10 @@ let HttpKernel = class HttpKernel extends Service {
887
966
  * N'attend que le repli statique, et seulement quand aucune route ne matche.
888
967
  */
889
968
  routeHttpRequest(httpContext, request, response) {
969
+ if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
970
+ const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
971
+ if (relayed !== void 0) return relayed.then(() => httpContext);
972
+ }
890
973
  httpContext.phaseStart("resolve");
891
974
  httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
892
975
  httpContext.phaseEnd("resolve");
@@ -931,7 +1014,7 @@ let HttpKernel = class HttpKernel extends Service {
931
1014
  if (error) throw error;
932
1015
  const schemeHeaders = context.scheme === "https" ? this.options.https.headers : context.scheme === "http" ? this.options.http.headers : null;
933
1016
  if (schemeHeaders) context.response.setHeaders(schemeHeaders);
934
- if (this.kernel?.options.domainCheck) this.checkValidDomain(context);
1017
+ if (this.domainCheck) this.checkValidDomain(context);
935
1018
  if (this.listenerCount("beforeResolve")) return this.fireAsync("beforeResolve", context).then(() => this.armAndGuard(context));
936
1019
  return this.armAndGuard(context);
937
1020
  }
@@ -942,7 +1025,7 @@ let HttpKernel = class HttpKernel extends Service {
942
1025
  armAndGuard(context) {
943
1026
  this.prepareFrontController(context);
944
1027
  this.firewall?.enforceCsrf(context);
945
- return thenMaybe(this.startSession(context), () => context.secure || context.isControlledAccess ? this.runFirewall(context) : context);
1028
+ return thenMaybe(this.startSession(context), () => context.secure ? this.runFirewall(context) : context);
946
1029
  }
947
1030
  /**
948
1031
  * Le pare-feu d'une requête en zone protégée : `handleSecurity`, puis les
@@ -958,9 +1041,15 @@ let HttpKernel = class HttpKernel extends Service {
958
1041
  await this.firewall?.handleSecurity(context);
959
1042
  if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
960
1043
  } catch (authError) {
1044
+ if (context.resolver?.exception) {
1045
+ context.response.response?.removeHeader("Upgrade");
1046
+ context.response.response?.removeHeader("Connection");
1047
+ }
961
1048
  await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
962
1049
  throw authError;
963
1050
  }
1051
+ const pending = context.resolver?.exception;
1052
+ if (pending) throw pending;
964
1053
  } finally {
965
1054
  context.phaseEnd("firewall");
966
1055
  }
@@ -996,33 +1085,48 @@ let HttpKernel = class HttpKernel extends Service {
996
1085
  this.log(e, "ERROR", "WS onFinish");
997
1086
  }
998
1087
  }
999
- 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
- }
1088
+ /**
1089
+ * Point d'entrée d'un upgrade vers une route WebSocket de Nodefony, appelé
1090
+ * par le répartiteur d'upgrade AVANT le `101` (#596) : rien ne s'ouvre tant
1091
+ * que le pipeline n'a pas dit oui.
1092
+ *
1093
+ * Les bornes par IP passent en premier, avant tout scope, ALS ou pipeline
1094
+ * (symétrie avec le 429 HTTP et avec le proxy inverse) : le vrai vecteur DoS
1095
+ * est d'accumuler des sockets ouvertes. La place est rendue à la fermeture du
1096
+ * socket du client — même règle que pour un tunnel relayé.
1097
+ *
1098
+ * @param req - requête d'upgrade
1099
+ * @param admission - le verdict à rendre : accepter (`101`) ou refuser (HTTP)
1100
+ * @param type - serveur qui a reçu l'upgrade
1101
+ * @returns la promesse du traitement de la connexion
1102
+ */
1103
+ async onWebsocketUpgrade(req, admission, type) {
1104
+ if (this.websocketQuotaRefusal(req, req.socket) !== null) {
1105
+ admission.refuse(429);
1106
+ return;
1016
1107
  }
1017
1108
  await this.fireAsync("onServerRequest", req, null, type);
1018
1109
  const scope = this.container?.enterScope("request");
1019
- return this.handleWebsocket(scope, ws, req, type);
1110
+ return this.handleWebsocket(scope, req, admission, type);
1020
1111
  }
1021
- async handleWebsocket(scope, ws, req, type) {
1112
+ /**
1113
+ * Pipeline d'un handshake WebSocket, dans la bulle ALS de la connexion :
1114
+ * jugement (`onConnect`), pare-feu, gardes de l'action — PUIS seulement
1115
+ * l'acceptation, la socket et l'action de handshake. Tout refus antérieur à
1116
+ * l'acceptation part en statut HTTP (`onError` → `refuseHandshake`).
1117
+ *
1118
+ * @param scope - scope DI de la connexion
1119
+ * @param req - requête d'upgrade
1120
+ * @param admission - le verdict à rendre
1121
+ * @param type - serveur qui a reçu l'upgrade
1122
+ * @returns ce que rend l'action de handshake
1123
+ */
1124
+ async handleWebsocket(scope, req, admission, type) {
1022
1125
  let context = null;
1023
1126
  let error = null;
1024
1127
  try {
1025
- context = this.createWebsocketContext(scope, req, ws, type);
1128
+ context = this.createWebsocketContext(scope, req, null, type);
1129
+ context.admission = admission;
1026
1130
  } catch (e) {
1027
1131
  error = e;
1028
1132
  }
@@ -1034,15 +1138,17 @@ let HttpKernel = class HttpKernel extends Service {
1034
1138
  const wsRunId = context?.requestId ?? "ws-no-ctx";
1035
1139
  const wsScheme = context?.scheme ?? "ws";
1036
1140
  const wsTrace = context?.traceparent ?? null;
1037
- return await RequestContext.run({
1141
+ const store = {
1038
1142
  requestId: wsRunId,
1039
1143
  scheme: wsScheme,
1040
1144
  ...wsTrace ? { traceparent: wsTrace } : {},
1041
1145
  context,
1042
1146
  scope
1043
- }, async () => {
1147
+ };
1148
+ context?.holdRequestStore(store);
1149
+ return await RequestContext.run(store, async () => {
1044
1150
  await this.onConnect(context, error);
1045
- if (this.firewall && (context?.secure || context?.isControlledAccess)) {
1151
+ if (this.firewall && context?.secure) {
1046
1152
  context.phaseStart("firewall");
1047
1153
  try {
1048
1154
  try {
@@ -1056,22 +1162,34 @@ let HttpKernel = class HttpKernel extends Service {
1056
1162
  context.phaseEnd("firewall");
1057
1163
  }
1058
1164
  }
1059
- if (context) {
1060
- context.phaseStart("action");
1061
- try {
1062
- return await context.handle();
1063
- } finally {
1064
- context.phaseEnd("action");
1065
- }
1165
+ if (!context) return;
1166
+ const guarded = context.resolver?.authorizeAction();
1167
+ if (guarded !== void 0) await guarded;
1168
+ const accepted = admission.accept();
1169
+ const ws = accepted instanceof Promise ? await accepted : accepted;
1170
+ if (ws === null) {
1171
+ this.releaseOrphanWsScope(scope, context);
1172
+ return;
1173
+ }
1174
+ trackPong(ws);
1175
+ context.acceptConnection(ws);
1176
+ await context.connect();
1177
+ context.phaseStart("action");
1178
+ try {
1179
+ return await context.handle();
1180
+ } finally {
1181
+ context.phaseEnd("action");
1066
1182
  }
1067
1183
  });
1068
1184
  } catch (e) {
1069
1185
  try {
1070
1186
  await this.onError(e, context);
1071
1187
  } catch (errorHandlingError) {
1188
+ admission.refuse(500);
1072
1189
  this.releaseOrphanWsScope(scope, context);
1073
1190
  throw errorHandlingError;
1074
1191
  }
1192
+ admission.refuse(500);
1075
1193
  this.releaseOrphanWsScope(scope, context);
1076
1194
  throw e;
1077
1195
  }
@@ -1086,6 +1204,16 @@ let HttpKernel = class HttpKernel extends Service {
1086
1204
  this.container?.leaveScope(scope);
1087
1205
  context.clean();
1088
1206
  }
1207
+ /**
1208
+ * Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
1209
+ * `beforeResolve`, contrôleur (protocole négocié), session. N'ouvre rien —
1210
+ * l'acceptation n'a lieu qu'après le pare-feu et les gardes de l'action
1211
+ * (cf {@link handleWebsocket}).
1212
+ *
1213
+ * @param context - contexte du handshake, encore sans socket
1214
+ * @param error - erreur levée à la construction du contexte, relancée telle quelle
1215
+ * @throws la première erreur du jugement — rendue en statut HTTP par `onError`
1216
+ */
1089
1217
  async onConnect(context, error = null) {
1090
1218
  if (error) throw error;
1091
1219
  if (!context) throw new nodefonyError("Bad context", 500);
@@ -1099,7 +1227,6 @@ let HttpKernel = class HttpKernel extends Service {
1099
1227
  throw e;
1100
1228
  }
1101
1229
  if (!context.sessionStarting) await this.startSession(context);
1102
- return context.connect();
1103
1230
  }
1104
1231
  checkValidDomain(context) {
1105
1232
  if (context.validDomain) return 200;
@@ -1126,8 +1253,130 @@ let HttpKernel = class HttpKernel extends Service {
1126
1253
  isTrustedHostname(hostname) {
1127
1254
  return isDomainAllowed(this.regAlias, hostname);
1128
1255
  }
1256
+ /**
1257
+ * Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
1258
+ * (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
1259
+ * inverse et avant le serveur `ws`.
1260
+ *
1261
+ * Pourquoi avant : accepter puis fermer fait croire au client que le serveur
1262
+ * a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
1263
+ * une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
1264
+ * recharger la page avant que Vite soit prêt — page blanche.
1265
+ *
1266
+ * Ne juge que l'existence d'une route WebSocket pour le chemin
1267
+ * ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
1268
+ * quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
1269
+ * compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
1270
+ * flot d'upgrades vers des chemins inexistants y échapperait.
1271
+ *
1272
+ * @param req - requête d'upgrade
1273
+ * @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
1274
+ * inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
1275
+ * ou `null` si l'upgrade est admis
1276
+ */
1277
+ websocketUpgradeRefusal(req) {
1278
+ const router = this.router;
1279
+ if (router === null || router === void 0) return 503;
1280
+ let pathname;
1281
+ try {
1282
+ pathname = parseWebsocketUrl("ws", req.headers.host, req.url).pathname;
1283
+ } catch {
1284
+ return this.#rateLimitedRefusal(req) ?? 400;
1285
+ }
1286
+ if (router.servesWebsocket(pathname)) return null;
1287
+ return this.#rateLimitedRefusal(req) ?? 404;
1288
+ }
1289
+ /** Compte un upgrade refusé au limiteur de débit : `429` s'il le dépasse. */
1290
+ #rateLimitedRefusal(req) {
1291
+ if (this.rateLimiter === null) return null;
1292
+ const ip = this.#clientIp(req);
1293
+ return ip !== null && this.rateLimiter.hit(ip).limited ? 429 : null;
1294
+ }
1295
+ /**
1296
+ * Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
1297
+ * compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
1298
+ * simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
1299
+ * le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
1300
+ * qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
1301
+ * servie.
1302
+ *
1303
+ * L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
1304
+ * un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
1305
+ * par refus : sous un flood, il serait lui-même un amplificateur.
1306
+ *
1307
+ * @param req - requête d'upgrade
1308
+ * @param connection - ce dont la fermeture libère la place comptée (le `ws`
1309
+ * accepté, ou le socket du client pour un tunnel relayé)
1310
+ * @returns `null` si l'upgrade est admis — sa place est alors rendue à la
1311
+ * fermeture de `connection` —, sinon la raison du refus
1312
+ */
1313
+ websocketQuotaRefusal(req, connection) {
1314
+ if (this.rateLimiter === null && this.#wsConnCounter === null) return null;
1315
+ const ip = this.#clientIp(req);
1316
+ if (ip === null) return null;
1317
+ if (this.rateLimiter?.hit(ip).limited) return "rate limit";
1318
+ return this.#acquireSlot(ip, connection) ? null : "too many connections";
1319
+ }
1320
+ /**
1321
+ * Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
1322
+ * `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
1323
+ * un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
1324
+ * Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
1325
+ * à l'entrée du pipeline.
1326
+ *
1327
+ * @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
1328
+ * @param connection - ce dont la fermeture rend la place (la réponse)
1329
+ * @returns `true` si la place est prise — ou si aucun plafond n'est armé
1330
+ */
1331
+ acquireConnectionSlot(req, connection) {
1332
+ if (this.#wsConnCounter === null) return true;
1333
+ const ip = this.#clientIp(req);
1334
+ return ip === null || this.#acquireSlot(ip, connection);
1335
+ }
1336
+ /** IP cliente, résolue comme en HTTP (RFC 7239 + `trustProxy`). */
1337
+ #clientIp(req) {
1338
+ return resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
1339
+ }
1340
+ /**
1341
+ * Compte une connexion de `ip` ; sa place est rendue à la fermeture de
1342
+ * `connection`. Le compteur est capturé : la connexion décrémente TOUJOURS
1343
+ * l'instance qui l'a comptée, même après reconfiguration — et `once("close")`
1344
+ * part à toute fermeture (`terminate` du battement, rejet du pare-feu…).
1345
+ */
1346
+ #acquireSlot(ip, connection) {
1347
+ const counter = this.#wsConnCounter;
1348
+ if (counter === null) return true;
1349
+ if (!counter.tryAcquire(ip)) return false;
1350
+ connection.once("close", () => counter.release(ip));
1351
+ return true;
1352
+ }
1353
+ /**
1354
+ * Inscrit un flux SSE à identité révocable : il sera fermé dans la fenêtre de
1355
+ * re-validation qui suit la mort de son identité (déconnexion, session
1356
+ * révoquée, jeton expiré ou annulé) — le même registre et la même règle que
1357
+ * les sockets WebSocket.
1358
+ *
1359
+ * @internal — appelé par le flux lui-même.
1360
+ */
1361
+ watchStream(stream) {
1362
+ (this.#streams ??= new RevocationWatch({
1363
+ isValid: (entry) => entry.revalidate(),
1364
+ revoke: (entry) => void entry.close(),
1365
+ identityOf: (entry) => entry.identifier ?? void 0
1366
+ })).register(stream);
1367
+ }
1368
+ /** Retire un flux du registre de révocation (à sa fermeture). @internal */
1369
+ unwatchStream(stream) {
1370
+ this.#streams?.unregister(stream);
1371
+ }
1372
+ /**
1373
+ * Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
1374
+ * Publique pour qu'un banc la déclenche sans attendre le tick.
1375
+ */
1376
+ revalidateStreams() {
1377
+ return this.#streams?.revalidate() ?? Promise.resolve();
1378
+ }
1129
1379
  };
1130
- HttpKernel = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], HttpKernel);
1380
+ HttpKernel = __decorate([injectable()], HttpKernel);
1131
1381
  var http_kernel_default = HttpKernel;
1132
- //#endregion
1133
- export { http_kernel_default as default };
1382
+ export { http_kernel_default as default, responseEnded };