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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +13 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +3 -3
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  12. package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
  13. package/dist/nodefony/service/audit-logger.js +23 -30
  14. package/dist/nodefony/service/certificates.js +97 -13
  15. package/dist/nodefony/service/error-renderer.js +29 -12
  16. package/dist/nodefony/service/http-kernel.js +198 -50
  17. package/dist/nodefony/service/pretty-request-logger.js +15 -13
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +3 -3
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +3 -3
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  29. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +78 -43
  36. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  37. package/dist/nodefony/src/context/forwarded.js +1 -2
  38. package/dist/nodefony/src/context/http/HttpContext.js +28 -6
  39. package/dist/nodefony/src/context/http/Request.js +33 -29
  40. package/dist/nodefony/src/context/http/Response.js +23 -6
  41. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +41 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/requestTrace.js +104 -0
  49. package/dist/nodefony/src/context/responseEnded.js +1 -2
  50. package/dist/nodefony/src/context/timingMode.js +21 -0
  51. package/dist/nodefony/src/context/transport.js +45 -0
  52. package/dist/nodefony/src/context/trustProxy.js +1 -2
  53. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  54. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  55. package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
  56. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  57. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  58. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  59. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  60. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  61. package/dist/nodefony/src/cookies/cookie.js +3 -7
  62. package/dist/nodefony/src/errors/httpError.js +1 -2
  63. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  64. package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
  65. package/dist/nodefony/src/profiler/Profiler.js +104 -32
  66. package/dist/nodefony/src/profiler/aggregate.js +442 -0
  67. package/dist/nodefony/src/proxy/forward.js +29 -5
  68. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  69. package/dist/nodefony/src/proxy/rules.js +12 -6
  70. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  71. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  72. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  73. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  74. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  75. package/dist/nodefony/src/servers/portBinder.js +41 -10
  76. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  77. package/dist/nodefony/src/session/session.js +2 -3
  78. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  79. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  80. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  81. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  82. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  83. package/dist/types/index.d.ts +9 -3
  84. package/dist/types/nodefony/config/config.d.ts +4 -0
  85. package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
  86. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  87. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  88. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  89. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  91. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
  92. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  93. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  94. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  95. package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
  96. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  97. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  98. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  99. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  100. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  101. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  102. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  103. package/dist/types/nodefony/src/context/Context.d.ts +38 -5
  104. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
  105. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  106. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  107. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  108. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  109. package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
  110. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  111. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  112. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  113. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
  118. package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
  119. package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
  120. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  121. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  122. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  123. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  124. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  125. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  126. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  127. package/docs/cookies.md +85 -78
  128. package/docs/diagnostics-channel.md +303 -0
  129. package/docs/index.md +6 -1
  130. package/docs/observabilite.md +159 -81
  131. package/docs/rate-limit.md +83 -83
  132. package/docs/reverse-proxy.md +72 -70
  133. package/docs/servers.md +338 -224
  134. package/docs/session.md +179 -179
  135. package/docs/sse.md +24 -20
  136. package/docs/upload.md +120 -117
  137. package/package.json +8 -9
  138. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,7 +1,7 @@
1
1
  import HttpError from "../src/errors/httpError.js";
2
2
  import { toWsCloseCode } from "../src/context/websocket/WebsocketContext.js";
3
3
  import { Nodefony, nodefonyError } from "nodefony";
4
- //#region nodefony/service/error-renderer.ts
4
+ // nodefony/service/error-renderer.ts
5
5
  /** Une donnée rejetée par la validation n'est pas une requête malformée : 422, pas 400. */
6
6
  const VALIDATION_STATUS = 422;
7
7
  /**
@@ -74,8 +74,15 @@ const INTERNAL_ERROR_KEYS = [
74
74
  * deux tests — un gate qu'on ne peut plus voir mordre.
75
75
  */
76
76
  const isProduction = () => Nodefony.getKernel()?.environment === "production";
77
+ /** Une propriété d'une anomalie de forme inconnue, lue sans rien supposer. */
78
+ function readIssueProperty(issue, key) {
79
+ return typeof issue === "object" && issue !== null ? Reflect.get(issue, key) : void 0;
80
+ }
81
+ /** Erreurs de validation reconnues : Zod, et celle des routes à schéma (cœur). */
82
+ const VALIDATION_ERROR_NAMES = /* @__PURE__ */ new Set(["ZodError", "SchemaValidationError"]);
77
83
  /**
78
- * Reconnaît une erreur de validation Zod et en extrait les champs fautifs.
84
+ * Reconnaît une erreur de validation — `ZodError`, ou `SchemaValidationError`
85
+ * d'une route à schéma — et en extrait les champs fautifs.
79
86
  *
80
87
  * Reconnaissance **structurelle** (`name` + `issues`), pas `instanceof ZodError` :
81
88
  * une application peut embarquer sa propre copie de zod (résolutions npm multiples),
@@ -88,13 +95,21 @@ const isProduction = () => Nodefony.getKernel()?.environment === "production";
88
95
  * @returns les champs fautifs, ou `null` si ce n'est pas une erreur de validation.
89
96
  */
90
97
  function toValidationFields(error) {
91
- const candidate = error;
92
- if (error.name !== "ZodError" || !Array.isArray(candidate.issues)) return null;
93
- return candidate.issues.map((issue) => ({
94
- field: Array.isArray(issue.path) ? issue.path.join(".") : "",
95
- message: issue.message ?? "invalide",
96
- rule: issue.code
97
- }));
98
+ const issues = Reflect.get(error, "issues");
99
+ if (!VALIDATION_ERROR_NAMES.has(error.name) || !Array.isArray(issues)) return null;
100
+ return issues.map((issue) => {
101
+ const path = readIssueProperty(issue, "path");
102
+ const message = readIssueProperty(issue, "message");
103
+ const code = readIssueProperty(issue, "code");
104
+ const part = readIssueProperty(issue, "in");
105
+ const field = {
106
+ field: Array.isArray(path) ? path.map(String).join(".") : "",
107
+ message: typeof message === "string" ? message : "invalide",
108
+ rule: typeof code === "string" ? code : void 0
109
+ };
110
+ if (typeof part === "string") field.in = part;
111
+ return field;
112
+ });
98
113
  }
99
114
  /**
100
115
  * Construit l'erreur **422** exposée au client à partir des champs fautifs.
@@ -109,7 +124,10 @@ function toValidationFields(error) {
109
124
  * reçoit `error.fields` et sait QUEL champ corriger — pas seulement que « ça a échoué ».
110
125
  */
111
126
  function toValidationError(error, fields) {
112
- const summary = fields.map((f) => f.field ? `${f.field}: ${f.message}` : f.message).join(" · ");
127
+ const summary = fields.map((f) => {
128
+ const where = f.in && f.field ? `${f.in}.${f.field}` : f.in || f.field;
129
+ return where ? `${where}: ${f.message}` : f.message;
130
+ }).join(" · ");
113
131
  const validation = new nodefonyError(`Validation failed — ${summary}`, VALIDATION_STATUS);
114
132
  if (error.stack !== void 0) validation.stack = error.stack;
115
133
  validation.fields = fields;
@@ -429,5 +447,4 @@ var DefaultErrorRenderer = class {
429
447
  return code;
430
448
  }
431
449
  };
432
- //#endregion
433
- export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf };
450
+ export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf, toValidationFields };
@@ -1,3 +1,4 @@
1
+ import { isProfiledUrl } from "../src/profiler/aggregate.js";
1
2
  import HttpError from "../src/errors/httpError.js";
2
3
  import { buildTrustProxy } from "../src/context/trustProxy.js";
3
4
  import { resolveForwarded } from "../src/context/forwarded.js";
@@ -5,22 +6,23 @@ import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
5
6
  import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
6
7
  import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
7
8
  import { responseEnded } from "../src/context/responseEnded.js";
9
+ import { unsentResponseWarning } from "../src/context/unsentResponse.js";
10
+ import { closeRequestTrace, hasRequestTraceSubscribers, openRequestTrace, runTraced } from "../src/context/requestTrace.js";
8
11
  import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
9
- import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
12
+ import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
13
+ import { trackPong } from "./servers/wsHeartbeat.js";
10
14
  import HttpContext from "../src/context/http/HttpContext.js";
11
15
  import DefaultErrorRenderer from "./error-renderer.js";
12
- import DefaultRequestLogger from "./request-logger.js";
13
16
  import JsonAuditLogger from "./audit-logger.js";
17
+ import DefaultRequestLogger from "./request-logger.js";
14
18
  import PrettyRequestLogger from "./pretty-request-logger.js";
15
19
  import { resolveTraceparent } from "./trace.js";
16
20
  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";
21
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
22
+ import { GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
20
23
  import cluster from "node:cluster";
21
24
  import http from "node:http";
22
- import Ws from "ws";
23
- //#region nodefony/service/http-kernel.ts
25
+ // nodefony/service/http-kernel.ts
24
26
  const HEALTH_HEADERS = {
25
27
  "content-type": "application/json",
26
28
  "cache-control": "no-store"
@@ -111,6 +113,8 @@ let HttpKernel = class HttpKernel extends Service {
111
113
  errorRenderer = new DefaultErrorRenderer();
112
114
  requestLogger = new DefaultRequestLogger();
113
115
  profiler = null;
116
+ /** `timing.tooling` : profiler aussi l'outillage (lu une fois, à onReady). */
117
+ profileTooling = false;
114
118
  _trustProxyChecker = null;
115
119
  _wsOriginPolicy = Object.create(null);
116
120
  rateLimiter = null;
@@ -126,9 +130,7 @@ let HttpKernel = class HttpKernel extends Service {
126
130
  this.container?.addScope("request");
127
131
  this.responseTimeout = {
128
132
  http: this.options.http.responseTimeout,
129
- https: this.options.https.responseTimeout,
130
- http2: this.options.https.responseTimeout,
131
- http3: this.options.https.responseTimeout
133
+ https: this.options.https.responseTimeout
132
134
  };
133
135
  this.closeTimeOutWs = {
134
136
  ws: this.options.websocket.closeTimeout,
@@ -210,6 +212,36 @@ let HttpKernel = class HttpKernel extends Service {
210
212
  get rateLimitStore() {
211
213
  return this.rateLimiter;
212
214
  }
215
+ /**
216
+ * Cette requête — ou cette frame du pont — se profile-t-elle ? Jamais sans
217
+ * profileur (production). L'outillage (console, profileur, journaux, Vite :
218
+ * `isProfiledUrl`, sur la règle `isConsolePath` du cœur) seulement sous
219
+ * `timing.tooling` — on ne
220
+ * mesure pas la façon dont on mesure : ni tampon SQL, ni instantané, ni place
221
+ * dans les anneaux du profileur.
222
+ *
223
+ * @param url - URL ou chemin de la requête
224
+ * @returns `true` quand le profil doit être pris
225
+ */
226
+ shouldProfile(url) {
227
+ return this.profiler !== null && isProfiledUrl(url, this.profileTooling);
228
+ }
229
+ /**
230
+ * En développement, signale une action qui a rendu une valeur que le
231
+ * Resolver ne sait pas envoyer (`null`, `undefined`, instance de classe)
232
+ * et n'a rien envoyé elle-même : la requête attend son délai d'expiration.
233
+ * Le message nomme l'action, ce qu'elle a rendu et le geste qui convient
234
+ * (`unsentResponseWarning`).
235
+ *
236
+ * Appelé au timeout, AVANT que le 504/408 parte — après, `sended` est vrai et
237
+ * plus rien ne se signalerait — et au démontage, pour un client parti avant.
238
+ * Gratuit en production : rien n'est lu au-delà de `waitAsync`.
239
+ *
240
+ * @param context - le contexte HTTP de la requête
241
+ */
242
+ warnUnsentResponse(context) {
243
+ if (context.waitAsync && !context.sended && this.kernel?.environment === "development") this.log(unsentResponseWarning(context.resolver, context.url), "WARNING");
244
+ }
213
245
  configureRateLimit() {
214
246
  this.rateLimitGc?.stop();
215
247
  this.rateLimitGc = null;
@@ -280,6 +312,8 @@ let HttpKernel = class HttpKernel extends Service {
280
312
  this.regAlias = this.compileAlias();
281
313
  this.sessionService = this.get("sessions");
282
314
  this.profiler = this.get("profiler");
315
+ const timing = this.kernel?.options.timing;
316
+ this.profileTooling = typeof timing === "object" && timing !== null && "tooling" in timing && timing.tooling === true;
283
317
  this.configureRateLimit();
284
318
  this.configureWsConnectionLimit();
285
319
  this.configureHealth();
@@ -356,7 +390,7 @@ let HttpKernel = class HttpKernel extends Service {
356
390
  const policy = this.getWsOriginPolicy(secure ? "websocketSecure" : "websocket");
357
391
  if (policy.disabled) return true;
358
392
  if (!origin) return true;
359
- let originHost = null;
393
+ let originHost;
360
394
  try {
361
395
  originHost = new URL(origin).hostname;
362
396
  } catch {
@@ -382,7 +416,7 @@ let HttpKernel = class HttpKernel extends Service {
382
416
  * @throws {HttpError} code WS 1008 (Policy Violation) si l'Origin est refusée.
383
417
  */
384
418
  checkWebsocketOrigin(context) {
385
- const secure = context.type === "websocket-secure";
419
+ const secure = context.serverType === "wss";
386
420
  const raw = context.origin;
387
421
  if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
388
422
  const error = new HttpError(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
@@ -481,7 +515,14 @@ let HttpKernel = class HttpKernel extends Service {
481
515
  context.resolver = resolver;
482
516
  return resolver;
483
517
  }
484
- if (resolver.exception) throw resolver.exception;
518
+ if (resolver.exception) {
519
+ const exception = resolver.exception;
520
+ if (context.secure && "code" in exception && exception.code === 426) {
521
+ context.resolver = resolver;
522
+ return resolver;
523
+ }
524
+ throw resolver.exception;
525
+ }
485
526
  throw new HttpError("Not Found", 404, context);
486
527
  }
487
528
  /**
@@ -580,18 +621,26 @@ let HttpKernel = class HttpKernel extends Service {
580
621
  await context.close();
581
622
  return context;
582
623
  }
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);
624
+ if (context instanceof WebsocketContext) {
625
+ if (context.admission !== null) {
626
+ const status = toHandshakeStatus("code" in error && typeof error.code === "number" ? error.code : void 0);
627
+ context.refuseHandshake(status);
628
+ if (this.kernel?.debug) this.log(`${status} ${error.message}`, "DEBUG", "WEBSOCKET REFUSED");
587
629
  return context;
588
630
  }
589
- if (context.request && !context.rejected) {
590
- context.reject(wsResult.code, wsResult.reason);
591
- return context;
631
+ try {
632
+ const wsResult = this.errorRenderer.renderWebsocket(error, context);
633
+ if (context.response?.connection) {
634
+ context.close(wsResult.code, wsResult.reason);
635
+ return context;
636
+ }
637
+ if (context.request && !context.rejected) {
638
+ context.reject(wsResult.code, wsResult.reason);
639
+ return context;
640
+ }
641
+ } catch {
642
+ throw error;
592
643
  }
593
- } catch {
594
- throw error;
595
644
  }
596
645
  throw error;
597
646
  }
@@ -633,7 +682,7 @@ let HttpKernel = class HttpKernel extends Service {
633
682
  if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
634
683
  if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
635
684
  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);
685
+ if (this.secHsts !== null && type === "https") response.setHeader("Strict-Transport-Security", this.secHsts);
637
686
  if (this.#healthPaths !== null) {
638
687
  const url = request.url;
639
688
  if (url === this.#healthPaths.liveness) {
@@ -676,6 +725,11 @@ let HttpKernel = class HttpKernel extends Service {
676
725
  await serverHttps.createServer();
677
726
  servers.push(serverHttps);
678
727
  }
728
+ const serverH2c = this.get("server-h2c");
729
+ if (serverH2c?.active) {
730
+ await serverH2c.createServer();
731
+ servers.push(serverH2c);
732
+ }
679
733
  const serverWebsocket = this.get("server-websocket");
680
734
  if (serverWebsocket && serverHttp?.active) {
681
735
  await serverWebsocket.createServer(serverHttp);
@@ -686,7 +740,7 @@ let HttpKernel = class HttpKernel extends Service {
686
740
  await serverWebsocketSecure.createServer(serverHttps);
687
741
  servers.push(serverWebsocketSecure);
688
742
  }
689
- this.publishRuntimePorts(serverHttp, serverHttps);
743
+ this.publishRuntimePorts(serverHttp, serverHttps, serverH2c);
690
744
  return servers;
691
745
  }
692
746
  /**
@@ -720,7 +774,7 @@ let HttpKernel = class HttpKernel extends Service {
720
774
  * {@link readRuntimeState} — `status`/`stop` redeviendraient aveugles alors que
721
775
  * l'app écoute toujours. Le master, lui, est le propriétaire du runtime.
722
776
  */
723
- publishRuntimePorts(serverHttp, serverHttps) {
777
+ publishRuntimePorts(serverHttp, serverHttps, serverH2c) {
724
778
  const ports = [];
725
779
  const desired = [];
726
780
  const urls = [];
@@ -736,6 +790,11 @@ let HttpKernel = class HttpKernel extends Service {
736
790
  const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "https");
737
791
  if (port) desired.push(port);
738
792
  }
793
+ if (serverH2c?.active && serverH2c.port > 0) {
794
+ ports.push(serverH2c.port);
795
+ const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "h2c");
796
+ if (port) desired.push(port);
797
+ }
739
798
  if (ports.length === 0) return;
740
799
  writeRuntimeState(process.cwd(), {
741
800
  pid: cluster.isWorker ? process.ppid : process.pid,
@@ -794,10 +853,12 @@ let HttpKernel = class HttpKernel extends Service {
794
853
  teardownHttp(context, scope) {
795
854
  if (context.finished) return void 0;
796
855
  try {
797
- if (context.waitAsync && !context.sended && this.kernel?.environment === "development") this.log(`Action "${context.resolver?.route?.name ?? context.url}" returned a non-renderable value (void/null/class instance) and never sent a response. Use 'return <object|string|number|boolean|Buffer>' (auto-JSON) or send/stream manually.`, "WARNING");
856
+ context.stopClock();
857
+ if (context.waitAsync) this.warnUnsentResponse(context);
798
858
  context.logRequest();
859
+ if (context.requestTrace !== null) closeRequestTrace(context, context.response.statusCode, context.error);
799
860
  context.logPhasesVerbose();
800
- this.profiler?.collect(context);
861
+ if (this.shouldProfile(context.url)) this.profiler?.collect(context);
801
862
  const afterResponse = context._runAfterResponse();
802
863
  if (afterResponse !== void 0 || context.listenerCount("onFinish")) return this.finishTeardownHttp(context, scope, afterResponse);
803
864
  this.releaseHttpContext(context, scope);
@@ -856,7 +917,7 @@ let HttpKernel = class HttpKernel extends Service {
856
917
  response.on("close", () => {
857
918
  if (!responseEnded(response)) {
858
919
  context._abortIfPending("Connection closed before response finished");
859
- if (!context.sended) context.response.statusCode = 499;
920
+ if (context.response.statusCode < 400) context.response.statusCode = 499;
860
921
  }
861
922
  this.teardownHttp(context, scope);
862
923
  });
@@ -881,7 +942,7 @@ let HttpKernel = class HttpKernel extends Service {
881
942
  try {
882
943
  context = this.createHttpContext(scope, request, response, type);
883
944
  const created = context;
884
- const served = this.listenerCount("onCreateContext") ? this.fireAsync("onCreateContext", created).then(() => this.serveHttpContext(created, scope, request, response)) : this.serveHttpContext(created, scope, request, response);
945
+ const served = hasRequestTraceSubscribers() ? runTraced(openRequestTrace(created, "http"), this.dispatchHttpContext, this, created, scope, request, response) : this.dispatchHttpContext(created, scope, request, response);
885
946
  if (!isPromise(served)) return served;
886
947
  return served.catch((e) => this.failHttp(e, created, scope));
887
948
  } catch (e) {
@@ -889,12 +950,26 @@ let HttpKernel = class HttpKernel extends Service {
889
950
  }
890
951
  }
891
952
  /**
953
+ * Sert un contexte HTTP fraîchement créé : le crochet `onCreateContext`
954
+ * s'il a des écouteurs, puis le pipeline.
955
+ *
956
+ * Hot path : `fireAsync` est une fonction async → 1 Promise + 1 microtask
957
+ * MÊME à 0 listener. En prod sans @nodefony/security ces seams
958
+ * (onCreateContext/beforeResolve/afterAuth) n'ont aucun listener → garde
959
+ * `listenerCount` (O(1), 0 alloc) pour ne RIEN planifier.
960
+ *
961
+ * @returns le contexte servi, ou sa promesse
962
+ */
963
+ dispatchHttpContext(context, scope, request, response) {
964
+ return this.listenerCount("onCreateContext") ? this.fireAsync("onCreateContext", context).then(() => this.serveHttpContext(context, scope, request, response)) : this.serveHttpContext(context, scope, request, response);
965
+ }
966
+ /**
892
967
  * Traceparent, tampon du profileur, puis le pipeline dans la bulle ALS de la
893
968
  * requête (`RequestContext.run`).
894
969
  */
895
970
  serveHttpContext(context, scope, request, response) {
896
971
  context.traceparent = resolveTraceparent(request.headers.traceparent);
897
- const profilerQueries = this.profiler ? [] : null;
972
+ const profilerQueries = this.shouldProfile(context.url) ? [] : null;
898
973
  context.profilerQueries = profilerQueries;
899
974
  context.profiling = profilerQueries !== null;
900
975
  const store = {
@@ -946,7 +1021,10 @@ let HttpKernel = class HttpKernel extends Service {
946
1021
  routeHttpRequest(httpContext, request, response) {
947
1022
  if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
948
1023
  const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
949
- if (relayed !== void 0) return relayed.then(() => httpContext);
1024
+ if (relayed !== void 0) {
1025
+ httpContext.relayAccessLog = this.reverseProxy.match(request.url)?.accessLog ?? "all";
1026
+ return relayed.then(() => httpContext);
1027
+ }
950
1028
  }
951
1029
  httpContext.phaseStart("resolve");
952
1030
  httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
@@ -1019,9 +1097,15 @@ let HttpKernel = class HttpKernel extends Service {
1019
1097
  await this.firewall?.handleSecurity(context);
1020
1098
  if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
1021
1099
  } catch (authError) {
1100
+ if (context.resolver?.exception) {
1101
+ context.response.response?.removeHeader("Upgrade");
1102
+ context.response.response?.removeHeader("Connection");
1103
+ }
1022
1104
  await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
1023
1105
  throw authError;
1024
1106
  }
1107
+ const pending = context.resolver?.exception;
1108
+ if (pending) throw pending;
1025
1109
  } finally {
1026
1110
  context.phaseEnd("firewall");
1027
1111
  }
@@ -1057,21 +1141,48 @@ let HttpKernel = class HttpKernel extends Service {
1057
1141
  this.log(e, "ERROR", "WS onFinish");
1058
1142
  }
1059
1143
  }
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);
1144
+ /**
1145
+ * Point d'entrée d'un upgrade vers une route WebSocket de Nodefony, appelé
1146
+ * par le répartiteur d'upgrade AVANT le `101` (#596) : rien ne s'ouvre tant
1147
+ * que le pipeline n'a pas dit oui.
1148
+ *
1149
+ * Les bornes par IP passent en premier, avant tout scope, ALS ou pipeline
1150
+ * (symétrie avec le 429 HTTP et avec le proxy inverse) : le vrai vecteur DoS
1151
+ * est d'accumuler des sockets ouvertes. La place est rendue à la fermeture du
1152
+ * socket du client — même règle que pour un tunnel relayé.
1153
+ *
1154
+ * @param req - requête d'upgrade
1155
+ * @param admission - le verdict à rendre : accepter (`101`) ou refuser (HTTP)
1156
+ * @param type - serveur qui a reçu l'upgrade
1157
+ * @returns la promesse du traitement de la connexion
1158
+ */
1159
+ async onWebsocketUpgrade(req, admission, type) {
1160
+ if (this.websocketQuotaRefusal(req, req.socket) !== null) {
1161
+ admission.refuse(429);
1064
1162
  return;
1065
1163
  }
1066
1164
  await this.fireAsync("onServerRequest", req, null, type);
1067
1165
  const scope = this.container?.enterScope("request");
1068
- return this.handleWebsocket(scope, ws, req, type);
1166
+ return this.handleWebsocket(scope, req, admission, type);
1069
1167
  }
1070
- async handleWebsocket(scope, ws, req, type) {
1168
+ /**
1169
+ * Pipeline d'un handshake WebSocket, dans la bulle ALS de la connexion :
1170
+ * jugement (`onConnect`), pare-feu, gardes de l'action — PUIS seulement
1171
+ * l'acceptation, la socket et l'action de handshake. Tout refus antérieur à
1172
+ * l'acceptation part en statut HTTP (`onError` → `refuseHandshake`).
1173
+ *
1174
+ * @param scope - scope DI de la connexion
1175
+ * @param req - requête d'upgrade
1176
+ * @param admission - le verdict à rendre
1177
+ * @param type - serveur qui a reçu l'upgrade
1178
+ * @returns ce que rend l'action de handshake
1179
+ */
1180
+ async handleWebsocket(scope, req, admission, type) {
1071
1181
  let context = null;
1072
1182
  let error = null;
1073
1183
  try {
1074
- context = this.createWebsocketContext(scope, req, ws, type);
1184
+ context = this.createWebsocketContext(scope, req, null, type);
1185
+ context.admission = admission;
1075
1186
  } catch (e) {
1076
1187
  error = e;
1077
1188
  }
@@ -1091,7 +1202,7 @@ let HttpKernel = class HttpKernel extends Service {
1091
1202
  scope
1092
1203
  };
1093
1204
  context?.holdRequestStore(store);
1094
- return await RequestContext.run(store, async () => {
1205
+ const pipeline = async () => {
1095
1206
  await this.onConnect(context, error);
1096
1207
  if (this.firewall && context?.secure) {
1097
1208
  context.phaseStart("firewall");
@@ -1107,26 +1218,55 @@ let HttpKernel = class HttpKernel extends Service {
1107
1218
  context.phaseEnd("firewall");
1108
1219
  }
1109
1220
  }
1110
- if (context) {
1111
- context.phaseStart("action");
1112
- try {
1113
- return await context.handle();
1114
- } finally {
1115
- context.phaseEnd("action");
1116
- }
1221
+ if (!context) return;
1222
+ const guarded = context.resolver?.authorizeAction();
1223
+ if (guarded !== void 0) await guarded;
1224
+ const accepted = admission.accept();
1225
+ const ws = accepted instanceof Promise ? await accepted : accepted;
1226
+ if (ws === null) {
1227
+ if (context.requestTrace !== null) closeRequestTrace(context, null, null);
1228
+ this.releaseOrphanWsScope(scope, context);
1229
+ return;
1117
1230
  }
1118
- });
1231
+ trackPong(ws);
1232
+ context.acceptConnection(ws);
1233
+ await context.connect();
1234
+ context.phaseStart("action");
1235
+ try {
1236
+ return await context.handle();
1237
+ } finally {
1238
+ context.phaseEnd("action");
1239
+ }
1240
+ };
1241
+ return await (context !== null && hasRequestTraceSubscribers() ? runTraced(openRequestTrace(context, "websocket"), RequestContext.run, RequestContext, store, pipeline) : RequestContext.run(store, pipeline));
1119
1242
  } catch (e) {
1120
1243
  try {
1121
1244
  await this.onError(e, context);
1122
1245
  } catch (errorHandlingError) {
1246
+ admission.refuse(500);
1247
+ this.closeFailedWebsocketTrace(context, e);
1123
1248
  this.releaseOrphanWsScope(scope, context);
1124
1249
  throw errorHandlingError;
1125
1250
  }
1251
+ admission.refuse(500);
1252
+ this.closeFailedWebsocketTrace(context, e);
1126
1253
  this.releaseOrphanWsScope(scope, context);
1127
1254
  throw e;
1128
1255
  }
1129
1256
  }
1257
+ /**
1258
+ * Fin du canal `nodefony.http.request` pour un handshake interrompu par une
1259
+ * exception — sans effet si un refus l'a déjà publiée (`refuseHandshake`).
1260
+ * Le statut est un fait, pas une déduction : une socket rattachée dit que
1261
+ * le `101` est parti ; sinon c'est le `500` du filet qui a été rendu.
1262
+ *
1263
+ * @param context - le contexte du handshake, `null` s'il n'a pu être construit
1264
+ * @param error - l'exception qui a interrompu le pipeline
1265
+ */
1266
+ closeFailedWebsocketTrace(context, error) {
1267
+ if (!context?.requestTrace) return;
1268
+ closeRequestTrace(context, context.connection !== null ? 101 : 500, error);
1269
+ }
1130
1270
  releaseOrphanWsScope(scope, context) {
1131
1271
  if (!context) {
1132
1272
  this.container?.leaveScope(scope);
@@ -1137,6 +1277,16 @@ let HttpKernel = class HttpKernel extends Service {
1137
1277
  this.container?.leaveScope(scope);
1138
1278
  context.clean();
1139
1279
  }
1280
+ /**
1281
+ * Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
1282
+ * `beforeResolve`, contrôleur (protocole négocié), session. N'ouvre rien —
1283
+ * l'acceptation n'a lieu qu'après le pare-feu et les gardes de l'action
1284
+ * (cf {@link handleWebsocket}).
1285
+ *
1286
+ * @param context - contexte du handshake, encore sans socket
1287
+ * @param error - erreur levée à la construction du contexte, relancée telle quelle
1288
+ * @throws la première erreur du jugement — rendue en statut HTTP par `onError`
1289
+ */
1140
1290
  async onConnect(context, error = null) {
1141
1291
  if (error) throw error;
1142
1292
  if (!context) throw new nodefonyError("Bad context", 500);
@@ -1150,7 +1300,6 @@ let HttpKernel = class HttpKernel extends Service {
1150
1300
  throw e;
1151
1301
  }
1152
1302
  if (!context.sessionStarting) await this.startSession(context);
1153
- return context.connect();
1154
1303
  }
1155
1304
  checkValidDomain(context) {
1156
1305
  if (context.validDomain) return 200;
@@ -1301,7 +1450,6 @@ let HttpKernel = class HttpKernel extends Service {
1301
1450
  return this.#streams?.revalidate() ?? Promise.resolve();
1302
1451
  }
1303
1452
  };
1304
- HttpKernel = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], HttpKernel);
1453
+ HttpKernel = __decorate([injectable()], HttpKernel);
1305
1454
  var http_kernel_default = HttpKernel;
1306
- //#endregion
1307
1455
  export { http_kernel_default as default, responseEnded };
@@ -1,6 +1,7 @@
1
- import { computeDurationMs, severityFromStatus } from "./audit-logger.js";
2
- import { logColor } from "nodefony";
3
- //#region nodefony/service/pretty-request-logger.ts
1
+ import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
2
+ import { severityFromStatus, statusOf } from "./audit-logger.js";
3
+ import { accessStatusFamily, logColor } from "nodefony";
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
  *
@@ -23,9 +24,9 @@ var PrettyRequestLogger = class {
23
24
  renderHttp(context, error) {
24
25
  const ctx = context;
25
26
  const err = error ?? ctx.error ?? null;
26
- const status = ctx.response?.statusCode ?? (err ? 500 : null);
27
+ const status = statusOf(ctx) ?? (err ? 500 : null);
27
28
  const method = ctx.method ?? "?";
28
- const duration = computeDurationMs(ctx.phases);
29
+ const duration = ctx.stopClock();
29
30
  return {
30
31
  text: `${logColor.cyan(method.padEnd(4))} ${colorizeStatus(status)} ${ctx.url} ${logColor.blackBright(formatDuration(duration))} ${logColor.blackBright(ctx.remoteAddress ?? "-")} ` + logColor.magenta(`[${shortId(ctx.requestId)}]`) + (err ? ` ${logColor.red(err.message)}` : ""),
31
32
  severity: severityFromStatus(status),
@@ -34,22 +35,24 @@ 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 duration = computeDurationMs(ctx.phases);
38
+ const refused = error ? handshakeRefusalStatus(context, error) : null;
39
+ const status = refused ?? statusOf(ctx) ?? (error ? 500 : null);
40
+ const duration = ctx.stopClock();
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
  }
46
48
  };
47
49
  function colorizeStatus(status) {
48
- if (status === null) return logColor.blackBright("---");
50
+ const family = accessStatusFamily(status);
51
+ if (status === null || family === null) return logColor.blackBright("---");
49
52
  const s = String(status);
50
- if (status >= 500) return logColor.red(s);
51
- if (status >= 400) return logColor.yellowBold(s);
52
- if (status >= 300) return logColor.yellow(s);
53
+ if (family >= 5) return logColor.red(s);
54
+ if (family === 4) return logColor.yellowBold(s);
55
+ if (family === 3) return logColor.yellow(s);
53
56
  return logColor.green(s);
54
57
  }
55
58
  function formatDuration(ms) {
@@ -61,5 +64,4 @@ function formatDuration(ms) {
61
64
  function shortId(id) {
62
65
  return id.length > 8 ? id.slice(0, 8) : id;
63
66
  }
64
- //#endregion
65
67
  export { PrettyRequestLogger as default };