@nodefony/http 10.0.0-beta.3 → 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 (124) 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 +12 -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 +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -6,21 +6,20 @@ import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
6
6
  import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
7
7
  import { responseEnded } from "../src/context/responseEnded.js";
8
8
  import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
9
- import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
9
+ import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
10
+ import { trackPong } from "./servers/wsHeartbeat.js";
10
11
  import HttpContext from "../src/context/http/HttpContext.js";
11
12
  import DefaultErrorRenderer from "./error-renderer.js";
12
- import DefaultRequestLogger from "./request-logger.js";
13
13
  import JsonAuditLogger from "./audit-logger.js";
14
+ import DefaultRequestLogger from "./request-logger.js";
14
15
  import PrettyRequestLogger from "./pretty-request-logger.js";
15
16
  import { resolveTraceparent } from "./trace.js";
16
17
  import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
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
+ 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";
20
20
  import cluster from "node:cluster";
21
21
  import http from "node:http";
22
- import Ws from "ws";
23
- //#region nodefony/service/http-kernel.ts
22
+ // nodefony/service/http-kernel.ts
24
23
  const HEALTH_HEADERS = {
25
24
  "content-type": "application/json",
26
25
  "cache-control": "no-store"
@@ -126,9 +125,7 @@ let HttpKernel = class HttpKernel extends Service {
126
125
  this.container?.addScope("request");
127
126
  this.responseTimeout = {
128
127
  http: this.options.http.responseTimeout,
129
- https: this.options.https.responseTimeout,
130
- http2: this.options.https.responseTimeout,
131
- http3: this.options.https.responseTimeout
128
+ https: this.options.https.responseTimeout
132
129
  };
133
130
  this.closeTimeOutWs = {
134
131
  ws: this.options.websocket.closeTimeout,
@@ -356,7 +353,7 @@ let HttpKernel = class HttpKernel extends Service {
356
353
  const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
357
354
  if (policy.disabled) return true;
358
355
  if (!origin) return true;
359
- let originHost = null;
356
+ let originHost;
360
357
  try {
361
358
  originHost = new URL(origin).hostname;
362
359
  } catch {
@@ -382,7 +379,7 @@ let HttpKernel = class HttpKernel extends Service {
382
379
  * @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
383
380
  */
384
381
  checkWebsocketOrigin(context) {
385
- const secure = context.type === "websocket-secure";
382
+ const secure = context.serverType === "wss";
386
383
  const raw = context.origin;
387
384
  if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
388
385
  const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
@@ -481,7 +478,14 @@ let HttpKernel = class HttpKernel extends Service {
481
478
  context.resolver = resolver;
482
479
  return resolver;
483
480
  }
484
- 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
+ }
485
489
  throw new HttpError("Not Found", 404, context);
486
490
  }
487
491
  /**
@@ -580,18 +584,26 @@ let HttpKernel = class HttpKernel extends Service {
580
584
  await context.close();
581
585
  return context;
582
586
  }
583
- if (context instanceof WebsocketContext) try {
584
- const wsResult = this.errorRenderer.renderWebsocket(error, context);
585
- if (context.response?.connection) {
586
- 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");
587
592
  return context;
588
593
  }
589
- if (context.request && !context.rejected) {
590
- context.reject(wsResult.code, wsResult.reason);
591
- 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;
592
606
  }
593
- } catch {
594
- throw error;
595
607
  }
596
608
  throw error;
597
609
  }
@@ -633,7 +645,7 @@ let HttpKernel = class HttpKernel extends Service {
633
645
  if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
634
646
  if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
635
647
  if (this.secFrameOptions !== null) response.setHeader("X-Frame-Options", this.secFrameOptions);
636
- 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);
637
649
  if (this.#healthPaths !== null) {
638
650
  const url = request.url;
639
651
  if (url === this.#healthPaths.liveness) {
@@ -676,6 +688,11 @@ let HttpKernel = class HttpKernel extends Service {
676
688
  await serverHttps.createServer();
677
689
  servers.push(serverHttps);
678
690
  }
691
+ const serverH2c = this.get("server-h2c");
692
+ if (serverH2c?.active) {
693
+ await serverH2c.createServer();
694
+ servers.push(serverH2c);
695
+ }
679
696
  const serverWebsocket = this.get("server-websocket");
680
697
  if (serverWebsocket && serverHttp?.active) {
681
698
  await serverWebsocket.createServer(serverHttp);
@@ -686,7 +703,7 @@ let HttpKernel = class HttpKernel extends Service {
686
703
  await serverWebsocketSecure.createServer(serverHttps);
687
704
  servers.push(serverWebsocketSecure);
688
705
  }
689
- this.publishRuntimePorts(serverHttp, serverHttps);
706
+ this.publishRuntimePorts(serverHttp, serverHttps, serverH2c);
690
707
  return servers;
691
708
  }
692
709
  /**
@@ -720,7 +737,7 @@ let HttpKernel = class HttpKernel extends Service {
720
737
  * {@link readRuntimeState} — `status`/`stop` redeviendraient aveugles alors que
721
738
  * l'app écoute toujours. Le master, lui, est le propriétaire du runtime.
722
739
  */
723
- publishRuntimePorts(serverHttp, serverHttps) {
740
+ publishRuntimePorts(serverHttp, serverHttps, serverH2c) {
724
741
  const ports = [];
725
742
  const desired = [];
726
743
  const urls = [];
@@ -736,6 +753,11 @@ let HttpKernel = class HttpKernel extends Service {
736
753
  const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "https");
737
754
  if (port) desired.push(port);
738
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
+ }
739
761
  if (ports.length === 0) return;
740
762
  writeRuntimeState(process.cwd(), {
741
763
  pid: cluster.isWorker ? process.ppid : process.pid,
@@ -856,7 +878,7 @@ let HttpKernel = class HttpKernel extends Service {
856
878
  response.on("close", () => {
857
879
  if (!responseEnded(response)) {
858
880
  context._abortIfPending("Connection closed before response finished");
859
- if (!context.sended) context.response.statusCode = 499;
881
+ if (context.response.statusCode < 400) context.response.statusCode = 499;
860
882
  }
861
883
  this.teardownHttp(context, scope);
862
884
  });
@@ -1019,9 +1041,15 @@ let HttpKernel = class HttpKernel extends Service {
1019
1041
  await this.firewall?.handleSecurity(context);
1020
1042
  if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
1021
1043
  } catch (authError) {
1044
+ if (context.resolver?.exception) {
1045
+ context.response.response?.removeHeader("Upgrade");
1046
+ context.response.response?.removeHeader("Connection");
1047
+ }
1022
1048
  await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
1023
1049
  throw authError;
1024
1050
  }
1051
+ const pending = context.resolver?.exception;
1052
+ if (pending) throw pending;
1025
1053
  } finally {
1026
1054
  context.phaseEnd("firewall");
1027
1055
  }
@@ -1057,21 +1085,48 @@ let HttpKernel = class HttpKernel extends Service {
1057
1085
  this.log(e, "ERROR", "WS onFinish");
1058
1086
  }
1059
1087
  }
1060
- async onWebsocketRequest(ws, req, type) {
1061
- const refusal = this.websocketQuotaRefusal(req, ws);
1062
- if (refusal !== null) {
1063
- if (ws.readyState === Ws.OPEN) ws.close(1013, refusal);
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);
1064
1106
  return;
1065
1107
  }
1066
1108
  await this.fireAsync("onServerRequest", req, null, type);
1067
1109
  const scope = this.container?.enterScope("request");
1068
- return this.handleWebsocket(scope, ws, req, type);
1110
+ return this.handleWebsocket(scope, req, admission, type);
1069
1111
  }
1070
- 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) {
1071
1125
  let context = null;
1072
1126
  let error = null;
1073
1127
  try {
1074
- context = this.createWebsocketContext(scope, req, ws, type);
1128
+ context = this.createWebsocketContext(scope, req, null, type);
1129
+ context.admission = admission;
1075
1130
  } catch (e) {
1076
1131
  error = e;
1077
1132
  }
@@ -1107,22 +1162,34 @@ let HttpKernel = class HttpKernel extends Service {
1107
1162
  context.phaseEnd("firewall");
1108
1163
  }
1109
1164
  }
1110
- if (context) {
1111
- context.phaseStart("action");
1112
- try {
1113
- return await context.handle();
1114
- } finally {
1115
- context.phaseEnd("action");
1116
- }
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");
1117
1182
  }
1118
1183
  });
1119
1184
  } catch (e) {
1120
1185
  try {
1121
1186
  await this.onError(e, context);
1122
1187
  } catch (errorHandlingError) {
1188
+ admission.refuse(500);
1123
1189
  this.releaseOrphanWsScope(scope, context);
1124
1190
  throw errorHandlingError;
1125
1191
  }
1192
+ admission.refuse(500);
1126
1193
  this.releaseOrphanWsScope(scope, context);
1127
1194
  throw e;
1128
1195
  }
@@ -1137,6 +1204,16 @@ let HttpKernel = class HttpKernel extends Service {
1137
1204
  this.container?.leaveScope(scope);
1138
1205
  context.clean();
1139
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
+ */
1140
1217
  async onConnect(context, error = null) {
1141
1218
  if (error) throw error;
1142
1219
  if (!context) throw new nodefonyError("Bad context", 500);
@@ -1150,7 +1227,6 @@ let HttpKernel = class HttpKernel extends Service {
1150
1227
  throw e;
1151
1228
  }
1152
1229
  if (!context.sessionStarting) await this.startSession(context);
1153
- return context.connect();
1154
1230
  }
1155
1231
  checkValidDomain(context) {
1156
1232
  if (context.validDomain) return 200;
@@ -1301,7 +1377,6 @@ let HttpKernel = class HttpKernel extends Service {
1301
1377
  return this.#streams?.revalidate() ?? Promise.resolve();
1302
1378
  }
1303
1379
  };
1304
- HttpKernel = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], HttpKernel);
1380
+ HttpKernel = __decorate([injectable()], HttpKernel);
1305
1381
  var http_kernel_default = HttpKernel;
1306
- //#endregion
1307
1382
  export { http_kernel_default as default, responseEnded };
@@ -1,6 +1,7 @@
1
+ import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
1
2
  import { computeDurationMs, severityFromStatus } from "./audit-logger.js";
2
3
  import { logColor } from "nodefony";
3
- //#region nodefony/service/pretty-request-logger.ts
4
+ // nodefony/service/pretty-request-logger.ts
4
5
  /**
5
6
  * Pretty single-line logger for dev (P3.2) — the biggest gain for humans.
6
7
  *
@@ -34,12 +35,13 @@ var PrettyRequestLogger = class {
34
35
  }
35
36
  renderWebsocket(context, error, acceptedProtocol) {
36
37
  const ctx = context;
37
- const status = ctx.response?.statusCode ?? (error ? 500 : null);
38
+ const refused = error ? handshakeRefusalStatus(context, error) : null;
39
+ const status = refused ?? ctx.response?.statusCode ?? (error ? 500 : null);
38
40
  const duration = computeDurationMs(ctx.phases);
39
41
  const proto = acceptedProtocol ? `[${acceptedProtocol}]` : "";
40
42
  return {
41
43
  text: `${logColor.cyan("WS ")} ${colorizeStatus(status)} ${ctx.url} ${logColor.blackBright(proto)} ${logColor.blackBright(formatDuration(duration))} ${logColor.blackBright(ctx.remoteAddress ?? "-")} ` + logColor.magenta(`[${shortId(ctx.requestId)}]`) + (error ? ` ${logColor.red(error.message)}` : ""),
42
- severity: error ? "ERROR" : "INFO",
44
+ severity: refused !== null ? severityFromStatus(refused) : error ? "ERROR" : "INFO",
43
45
  msgid: "req"
44
46
  };
45
47
  }
@@ -61,5 +63,4 @@ function formatDuration(ms) {
61
63
  function shortId(id) {
62
64
  return id.length > 8 ? id.slice(0, 8) : id;
63
65
  }
64
- //#endregion
65
66
  export { PrettyRequestLogger as default };
@@ -1,5 +1,15 @@
1
+ import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
2
+ import { severityFromStatus } from "./audit-logger.js";
1
3
  import { logColor } from "nodefony";
2
- //#region nodefony/service/request-logger.ts
4
+ // nodefony/service/request-logger.ts
5
+ /**
6
+ * Statut de la réponse : le contrat ne type `response` qu'en `object` ; une
7
+ * annotation (vérifiée, pas une assertion) le lit — le reste passe par le contrat, que le
8
+ * compilateur surveille.
9
+ */
10
+ const statusOf = (context) => {
11
+ return context.response?.statusCode;
12
+ };
3
13
  /**
4
14
  * Default Nodefony per-request logger — preserves the legacy colored format.
5
15
  *
@@ -13,14 +23,13 @@ import { logColor } from "nodefony";
13
23
  */
14
24
  var DefaultRequestLogger = class {
15
25
  renderHttp(context, error) {
16
- const ctx = context;
17
- const txt = `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}`;
18
- const err = error ?? ctx.error;
26
+ const txt = `${logColor.cyan("URL")} : ${context.url} ${logColor.cyan("FROM")} : ${context.remoteAddress} ${logColor.cyan("ORIGIN")} : ${context.originUrl?.host} ${logColor.cyan("ID")} : ${context.requestId}`;
27
+ const err = error ?? context.error;
19
28
  if (err) {
20
- const errCode = err.code ?? ctx.response?.statusCode ?? 500;
21
- const msgid = `${ctx.type} ${logColor.magenta(errCode)} ${logColor.red(ctx.method ?? "")}`;
29
+ const errCode = err.code ?? statusOf(context) ?? 500;
30
+ const msgid = `${context.serverType} ${logColor.magenta(errCode)} ${logColor.red(context.method ?? "")}`;
22
31
  return {
23
- text: ctx.kernel?.environment === "production" ? `${txt} ${err}` : `${txt}\n ${err}`,
32
+ text: context.kernel?.environment === "production" ? `${txt} ${err}` : `${txt}\n ${err}`,
24
33
  severity: "ERROR",
25
34
  msgid
26
35
  };
@@ -28,27 +37,26 @@ var DefaultRequestLogger = class {
28
37
  return {
29
38
  text: txt,
30
39
  severity: "INFO",
31
- msgid: `${ctx.type} ${logColor.magenta(ctx.response?.statusCode ?? "")} ${ctx.method}`
40
+ msgid: `${context.serverType} ${logColor.magenta(statusOf(context) ?? "")} ${context.method}`
32
41
  };
33
42
  }
34
43
  renderWebsocket(context, error, acceptedProtocol) {
35
- const ctx = context;
36
44
  if (error) {
37
- const errCode = error.code ?? ctx.response?.statusCode ?? 500;
38
- const msgid = `${ctx.type} ${logColor.magenta(errCode)} ${logColor.red(ctx.method ?? "")}`;
45
+ const refused = handshakeRefusalStatus(context, error);
46
+ const errCode = refused ?? error.code ?? statusOf(context) ?? 500;
47
+ const msgid = `${context.serverType} ${logColor.magenta(errCode)} ${logColor.red(context.method ?? "")}`;
39
48
  return {
40
- text: `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}\n ` + error.toString(),
41
- severity: "ERROR",
49
+ text: `${logColor.cyan("URL")} : ${context.url} ${logColor.cyan("FROM")} : ${context.remoteAddress} ${logColor.cyan("ORIGIN")} : ${context.originUrl?.host} ${logColor.cyan("ID")} : ${context.requestId}\n ` + error.toString(),
50
+ severity: refused !== null ? severityFromStatus(refused) : "ERROR",
42
51
  msgid
43
52
  };
44
53
  }
45
- const msgid = `${ctx.type} ${logColor.magenta(ctx.response?.statusCode ?? "")} ${ctx.method}`;
54
+ const msgid = `${context.serverType} ${logColor.magenta(statusOf(context) ?? "")} ${context.method}`;
46
55
  return {
47
- text: `${logColor.cyan("URL")} : ${ctx.url} ${logColor.cyan("Accept-Protocol")} : ${acceptedProtocol || "*"} ${logColor.cyan("FROM")} : ${ctx.remoteAddress} ${logColor.cyan("ORIGIN")} : ${ctx.originUrl?.host} ${logColor.cyan("ID")} : ${ctx.requestId}`,
56
+ text: `${logColor.cyan("URL")} : ${context.url} ${logColor.cyan("Accept-Protocol")} : ${acceptedProtocol || "*"} ${logColor.cyan("FROM")} : ${context.remoteAddress} ${logColor.cyan("ORIGIN")} : ${context.originUrl?.host} ${logColor.cyan("ID")} : ${context.requestId}`,
48
57
  severity: "INFO",
49
58
  msgid
50
59
  };
51
60
  }
52
61
  };
53
- //#endregion
54
62
  export { DefaultRequestLogger as default };
@@ -1,11 +1,12 @@
1
1
  import { isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem } from "../src/proxy/rules.js";
2
+ import { requestAuthority } from "../src/context/transport.js";
2
3
  import { forwardRequest, forwardUpgrade, refuseUpgrade } from "../src/proxy/forward.js";
3
4
  import { Service } from "nodefony";
4
5
  import http from "node:http";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import https from "node:https";
7
8
  import { TLSSocket } from "node:tls";
8
- //#region nodefony/service/reverse-proxy.ts
9
+ // nodefony/service/reverse-proxy.ts
9
10
  /** Replis si la section `proxy` manque (module construit hors configuration). */
10
11
  const DEFAULT_TIMEOUT_MS = 3e4;
11
12
  const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
@@ -240,7 +241,7 @@ var ReverseProxy = class extends Service {
240
241
  const target = this.targetOf(mount);
241
242
  if (target === void 0) return answerPlain(res, 503, "Service Unavailable", "1");
242
243
  if (isAmbiguousPath(req.url ?? "/")) return answerPlain(res, 400, "Bad Request");
243
- const host = req.headers.host ?? req.headers[":authority"];
244
+ const host = requestAuthority(req.headers);
244
245
  return forwardRequest(req, res, mount, target, this.fromOf(req, scheme, host), this.relayFor(mount, target));
245
246
  }
246
247
  /**
@@ -332,5 +333,4 @@ function hostnameOf(host) {
332
333
  return null;
333
334
  }
334
335
  }
335
- //#endregion
336
336
  export { ReverseProxy as default };
@@ -1,4 +1,5 @@
1
- //#region nodefony/service/servers/clientError.ts
1
+ import { isPeerGoneError } from "../../src/context/websocket/Response.js";
2
+ // nodefony/service/servers/clientError.ts
2
3
  /**
3
4
  * Gère l'event `clientError` d'un `http.Server` / `https.Server`.
4
5
  *
@@ -16,5 +17,23 @@ function handleClientError(error, socket) {
16
17
  const statusLine = error.code === "HPE_HEADER_OVERFLOW" ? "431 Request Header Fields Too Large" : "400 Bad Request";
17
18
  socket.end(`HTTP/1.1 ${statusLine}\r\nConnection: close\r\n\r\n`);
18
19
  }
19
- //#endregion
20
- export { handleClientError };
20
+ /**
21
+ * Gère l'event `sessionError` d'un serveur HTTP/2 (h2c ou TLS) : un pair PARTI
22
+ * se tait, toute autre erreur est rapportée.
23
+ *
24
+ * Une session réinitialisée par le pair n'a rien à diagnostiquer. C'est la
25
+ * signature d'une sonde de port — `nodefony status`, `doctor`, le superviseur
26
+ * de développement, la sonde `tcpSocket` d'un orchestrateur — qui se connecte
27
+ * puis ferme sans lire le `SETTINGS` que le serveur envoie dès l'ouverture : le
28
+ * système ferme alors par un RST plutôt qu'un FIN, et Node lève `ECONNRESET`.
29
+ * La rapporter imprimait une pile entière par sonde. La règle « client parti »
30
+ * est celle des frames WebSocket ({@link isPeerGoneError}), pas une copie.
31
+ *
32
+ * @param error - erreur de session émise par `node:http2`
33
+ * @param report - rapporte une erreur réelle, à la sévérité choisie par le serveur
34
+ */
35
+ function handleSessionError(error, report) {
36
+ if (isPeerGoneError(error)) return;
37
+ report(error);
38
+ }
39
+ export { handleClientError, handleSessionError };
@@ -0,0 +1,126 @@
1
+ import { asServersConfig, configuredServerPort, isH2cEnabled } from "../../src/servers/kernelServers.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
+ import { handleSessionError } from "./clientError.js";
4
+ import { createDrainTerminator } from "./serverShutdown.js";
5
+ import { bindWithFallback, buildBindPlan, h2cPortConflict } from "../../src/servers/portBinder.js";
6
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
7
+ import { http2ServerLimits, setHttp2SessionTimeout } from "../../src/servers/http2Limits.js";
8
+ import { BootConfigurationError, Inject, Service, nodefonyError } from "nodefony";
9
+ import http2 from "node:http2";
10
+ // nodefony/service/servers/server-h2c.ts
11
+ /**
12
+ * Serveur HTTP/2 en clair (h2c, connaissance préalable — RFC 9113 §3.3), sur un
13
+ * port DÉDIÉ (`servers.h2c`, 5153 par défaut), pour un proxy inverse qui parle
14
+ * HTTP/2 à ses amonts.
15
+ *
16
+ * Il partage le chemin HTTP/2 du 5152 — même `onHttpRequest`, mêmes limites
17
+ * anti-DoS (`http2ServerLimits`), même drain par GOAWAY — sans TLS : `serverType`
18
+ * vaut `"http"`, le schéma vient du transport ou d'un proxy de confiance, jamais
19
+ * du pseudo-en-tête `:scheme`. Le protocole du saut (`"h2c"`) se constate sur la
20
+ * requête (`transport.ts#alpnProtocolOf`).
21
+ *
22
+ * Hors de son périmètre, délibérément : HTTP/1.1 (refusé par la préface HTTP/2 —
23
+ * sondes et clients HTTP/1.1 restent sur le port HTTP), WebSocket (aucun CONNECT
24
+ * étendu, RFC 8441 : un proxy route `Upgrade: websocket` vers le port HTTP) et la
25
+ * bascule `Upgrade: h2c` (retirée par RFC 9113 §3.1).
26
+ */
27
+ let ServerH2c = class ServerH2c extends Service {
28
+ httpKernel;
29
+ module;
30
+ server = null;
31
+ httpTerminator = null;
32
+ port;
33
+ protocol = "2.0";
34
+ ready = false;
35
+ type = "http";
36
+ domain;
37
+ scheme = "http";
38
+ address = null;
39
+ family = null;
40
+ active = false;
41
+ infos = null;
42
+ constructor(module, httpKernel) {
43
+ super("server-h2c", module.container ?? void 0, module.notificationsCenter, module.config.http);
44
+ this.httpKernel = httpKernel;
45
+ this.module = module;
46
+ this.active = isH2cEnabled(asServersConfig(module.kernel?.options.servers)?.h2c);
47
+ this.port = configuredServerPort(asServersConfig(module.kernel?.options.servers), "h2c") ?? 0;
48
+ this.domain = this.module.kernel?.domain ?? null;
49
+ }
50
+ /**
51
+ * Crée et met en écoute le serveur h2c.
52
+ *
53
+ * @returns le serveur à l'écoute
54
+ * @throws BootConfigurationError si `servers.h2c.port` est celui du serveur
55
+ * HTTP ou HTTPS — un port partagé n'est pas un conflit à contourner
56
+ */
57
+ createServer() {
58
+ return new Promise((resolve, reject) => {
59
+ try {
60
+ if (!this.active) return reject(/* @__PURE__ */ new Error(`Server h2c is not allowed in config file`));
61
+ const domain = this.domain;
62
+ if (domain === null) return reject(/* @__PURE__ */ new Error(`Server h2c : aucun domaine d'écoute (kernel absent)`));
63
+ const servers = asServersConfig(this.module.kernel?.options.servers);
64
+ const conflict = h2cPortConflict(servers);
65
+ if (conflict !== null) return reject(new BootConfigurationError(`servers.h2c.port (${this.port}) est aussi celui de servers.${conflict}.port : HTTP/1.1 et h2c ne partagent pas un port. Choisir un port distinct pour servers.h2c.port.`));
66
+ if (!servers?.http) this.log("servers.h2c actif sans servers.http : les sondes de santé et les WebSocket (HTTP/1.1) n'ont plus de port où arriver.", "WARNING");
67
+ const server = http2.createServer(http2ServerLimits(this.module.config.http2));
68
+ this.server = server;
69
+ this.httpTerminator = createDrainTerminator(server, this.options.shutdownTimeout);
70
+ setHttp2SessionTimeout(server, this.options.timeout, () => {
71
+ this.fire("onTimeout", this);
72
+ });
73
+ server.on("request", (request, response) => {
74
+ this.httpKernel.onHttpRequest(request, response, this.type);
75
+ });
76
+ server.on("sessionError", (error) => {
77
+ handleSessionError(error, (e) => this.log(e, "DEBUG", "H2C sessionError"));
78
+ });
79
+ server.on("streamError", (error) => {
80
+ this.log(error, "ERROR", "H2C streamError");
81
+ });
82
+ this.module.fire("onCreateServer", this.type, this);
83
+ bindWithFallback(server, domain, buildBindPlan("h2c", servers, this.module.kernel?.environment)).then(({ address, shiftedFrom }) => {
84
+ this.infos = address;
85
+ this.port = address.port;
86
+ this.address = address.address;
87
+ this.family = familyOf(address);
88
+ if (shiftedFrom !== null) this.log(`Port ${shiftedFrom} déjà occupé → h2c écoute sur ${this.port}. Figer le port : servers.h2c.port ; échouer au lieu de glisser : servers.portPolicy = "strict".`, "WARNING");
89
+ this.ready = true;
90
+ this.module.fire("onServersReady", this.type, this);
91
+ server.on("error", (error) => {
92
+ this.log(new nodefonyError(error), "CRITIC");
93
+ });
94
+ resolve(server);
95
+ }).catch((e) => {
96
+ const error = e instanceof Error ? e : new Error(String(e));
97
+ this.log(new nodefonyError(error), "CRITIC");
98
+ server.close();
99
+ reject(error);
100
+ });
101
+ this.kernel?.once("onTerminate", async () => {
102
+ if (!this.server) return;
103
+ try {
104
+ await this.httpTerminator?.terminate();
105
+ this.log(`h2c SHUTDOWN PORT : ${this.port}`, "INFO");
106
+ } catch (e) {
107
+ this.log(e, "ERROR", "TERMINATE");
108
+ }
109
+ });
110
+ } catch (e) {
111
+ this.log(e, "CRITIC");
112
+ return reject(e instanceof Error ? e : new Error(String(e)));
113
+ }
114
+ });
115
+ }
116
+ showBanner() {
117
+ if (this.infos) this.log(`Server Listen on h2c://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
118
+ }
119
+ };
120
+ ServerH2c = __decorate([__decorateParam(1, Inject("HttpKernel"))], ServerH2c);
121
+ /** Famille d'une adresse d'écoute, lue sans conversion (`AddressInfo.family` est une chaîne). */
122
+ function familyOf(address) {
123
+ return address.family === "IPv6" ? "IPv6" : "IPv4";
124
+ }
125
+ var server_h2c_default = ServerH2c;
126
+ export { server_h2c_default as default };
@@ -1,14 +1,12 @@
1
1
  import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
2
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
3
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
4
- import http_kernel_default from "../http-kernel.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
5
3
  import { handleClientError } from "./clientError.js";
6
4
  import { createDrainTerminator } from "./serverShutdown.js";
7
5
  import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
8
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateParam.js";
9
- import { Module, Service, extend, inject, nodefonyError } from "nodefony";
6
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
7
+ import { Inject, Service, extend, nodefonyError } from "nodefony";
10
8
  import http from "node:http";
11
- //#region nodefony/service/servers/server-http.ts
9
+ // nodefony/service/servers/server-http.ts
12
10
  let ServerHttp = class ServerHttp extends Service {
13
11
  httpKernel;
14
12
  module;
@@ -129,7 +127,6 @@ let ServerHttp = class ServerHttp extends Service {
129
127
  if (this.infos) this.log(`Server Listen on ${this.scheme}://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
130
128
  }
131
129
  };
132
- ServerHttp = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], ServerHttp);
130
+ ServerHttp = __decorate([__decorateParam(1, Inject("HttpKernel"))], ServerHttp);
133
131
  var server_http_default = ServerHttp;
134
- //#endregion
135
132
  export { server_http_default as default };