@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,4 +1,5 @@
1
1
  import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
2
+ import { closeRequestTrace } from "../requestTrace.js";
2
3
  import Context from "../Context.js";
3
4
  import { toWsCloseReason } from "./wsCloseReason.js";
4
5
  import WebsocketResponse from "./Response.js";
@@ -6,11 +7,11 @@ import { sanitizeRequestId } from "../requestId.js";
6
7
  import { FrameProfile } from "../../profiler/FrameProfile.js";
7
8
  import { formatWsLogContent } from "./wsLogContent.js";
8
9
  import { parseWebsocketUrl } from "./websocketUrl.js";
9
- import { RequestContext, logColor } from "nodefony";
10
- import Ws from "ws";
10
+ import { RequestContext, frameRequestId, logColor } from "nodefony";
11
11
  import { AsyncResource } from "node:async_hooks";
12
12
  import url, { URL } from "node:url";
13
- //#region nodefony/src/context/websocket/WebsocketContext.ts
13
+ import Ws from "ws";
14
+ // nodefony/src/context/websocket/WebsocketContext.ts
14
15
  /**
15
16
  * Appelle l'action résolue et rend TOUJOURS une promesse.
16
17
  *
@@ -54,6 +55,51 @@ function toWsCloseCode(code) {
54
55
  return 1011;
55
56
  }
56
57
  /**
58
+ * Statut HTTP d'un handshake REFUSÉ avant le `101` (RFC 6455 §4.2.2) — la
59
+ * réciproque de {@link toWsCloseCode} : une erreur levée pendant la décision
60
+ * porte un code HTTP (pare-feu, garde) ou, par héritage, un code de fermeture
61
+ * WS (origine `1008`, protocole `1002`, introuvable `4004`) ; le client qui
62
+ * lit le statut doit y reconnaître le même sens.
63
+ *
64
+ * @param code - code porté par l'erreur
65
+ * @returns un statut HTTP 4xx/5xx
66
+ */
67
+ function toHandshakeStatus(code) {
68
+ if (typeof code !== "number" || !Number.isInteger(code)) return 500;
69
+ if (code >= 400 && code <= 599) return code;
70
+ switch (code) {
71
+ case 1002:
72
+ case 1003:
73
+ case 1007: return 400;
74
+ case 1008:
75
+ case 4003: return 403;
76
+ case 4001: return 401;
77
+ case 1009: return 413;
78
+ case 1013: return 503;
79
+ case 4004: return 404;
80
+ default: return 500;
81
+ }
82
+ }
83
+ /**
84
+ * Statut HTTP que reçoit le client d'un handshake refusé — ou `null` si la
85
+ * connexion était déjà acceptée.
86
+ *
87
+ * La ligne de journal d'un refus s'écrit AVANT `refuseHandshake` : sans ce
88
+ * calcul, elle affichait le code de fermeture par défaut (`1000`) en ERROR,
89
+ * pour un `400` que le client reçoit. Même conversion que le refus
90
+ * ({@link toHandshakeStatus}), donc même chiffre. Chemin d'ERREUR seulement :
91
+ * 1 comparaison et 1 `typeof`, aucune allocation.
92
+ *
93
+ * @param context - le contexte WS de la ligne
94
+ * @param error - l'erreur qui a interrompu le handshake
95
+ * @returns le statut HTTP du refus, ou `null` après l'acceptation
96
+ */
97
+ function handshakeRefusalStatus(context, error) {
98
+ if (context.webSocketState !== "handshake") return null;
99
+ const code = error.code;
100
+ return toHandshakeStatus(typeof code === "number" ? code : void 0);
101
+ }
102
+ /**
57
103
  * Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
58
104
  * premier message (kernel présent). `true` hors production : le Suivi de requête
59
105
  * (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
@@ -68,16 +114,21 @@ var WebsocketContext = class extends Context {
68
114
  rejected = false;
69
115
  teardownWired = false;
70
116
  connection = null;
117
+ /**
118
+ * Le verdict du handshake encore à rendre : posé par le noyau tant que le
119
+ * pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
120
+ * est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
121
+ */
122
+ admission = null;
71
123
  origin;
72
124
  proxy = null;
73
125
  forwarded = null;
74
126
  wsUrl = null;
75
- queryGet = {};
76
- queryRequest = {};
127
+ query = {};
77
128
  wsPath = "";
78
129
  #frameSeq = 0;
79
- constructor(scope, req, ws, type) {
80
- super(scope, type);
130
+ constructor(scope, req, ws, serverType) {
131
+ super(scope, serverType);
81
132
  this.webSocketState = "handshake";
82
133
  this.request = req;
83
134
  this.connection = ws;
@@ -89,18 +140,16 @@ var WebsocketContext = class extends Context {
89
140
  if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
90
141
  this.remoteAddress = this.getRemoteAddress();
91
142
  this.acceptedProtocol = req.headers["sec-websocket-protocol"];
92
- this.scheme = type === "websocket-secure" ? "wss" : "ws";
143
+ this.scheme = serverType === "wss" ? "wss" : "ws";
93
144
  const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
94
145
  if (incomingId) this.requestId = incomingId;
95
146
  this.wsUrl = parseWebsocketUrl(this.scheme, req.headers.host, req.url);
96
- this.queryGet = Object.fromEntries(this.wsUrl.searchParams.entries());
97
- this.queryRequest = { ...this.queryGet };
147
+ this.query = Object.fromEntries(this.wsUrl.searchParams.entries());
98
148
  this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
99
149
  this.url = url.format(this.wsUrl);
100
- this.port = parseInt(this.wsUrl.port, 10) || (type === "websocket-secure" ? 443 : 80);
150
+ this.port = parseInt(this.wsUrl.port, 10) || (serverType === "wss" ? 443 : 80);
101
151
  this.request.url = this.wsUrl;
102
- this.request.queryGet = this.queryGet;
103
- this.request.query = this.queryRequest;
152
+ this.request.query = this.query;
104
153
  this.request.path = this.wsPath;
105
154
  try {
106
155
  this.originUrl = new URL(this.origin);
@@ -125,22 +174,51 @@ var WebsocketContext = class extends Context {
125
174
  this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy.proxyVia}`, "DEBUG");
126
175
  }
127
176
  }
128
- log(pci, severity, msgid, msg) {
177
+ log(pci, severity, msgid, msg, moduleName) {
129
178
  if (!msgid) msgid = "WEBSOCKET CONTEXT";
130
- return super.log(pci, severity, msgid, msg);
179
+ return super.log(pci, severity, msgid, msg, moduleName);
131
180
  }
132
181
  logRequest(httpError, acceptedProtocol) {
133
182
  try {
134
183
  const logger = this.httpKernel?.getRequestLogger();
135
184
  if (!logger) return;
136
185
  const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
137
- const pdu = this.log(entry.text, entry.severity, entry.msgid);
186
+ const pdu = this.log(entry.text, entry.severity, entry.msgid, void 0, this.logModule ?? this.resolver?.route?.module?.name);
138
187
  pdu.requestId ??= this.requestId;
139
188
  return pdu;
140
189
  } catch {
141
190
  return;
142
191
  }
143
192
  }
193
+ /**
194
+ * Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
195
+ * contexte a été construit et jugé AVANT, sans elle.
196
+ *
197
+ * @param ws - la socket acceptée
198
+ */
199
+ acceptConnection(ws) {
200
+ this.admission = null;
201
+ this.connection = ws;
202
+ this.response?.setConnection(ws);
203
+ }
204
+ /**
205
+ * Refuse le handshake par un statut HTTP, avant tout `101` (RFC 6455
206
+ * §4.2.2) — avec les en-têtes posés pendant la décision (`WWW-Authenticate`
207
+ * du pare-feu, `Retry-After`).
208
+ *
209
+ * @param status - statut HTTP du refus
210
+ * @returns `false` si le verdict était déjà rendu (rien n'est écrit)
211
+ */
212
+ refuseHandshake(status) {
213
+ const admission = this.admission;
214
+ if (admission === null) return false;
215
+ this.admission = null;
216
+ this.rejected = true;
217
+ this.webSocketState = "error";
218
+ admission.refuse(status, this.response?.handshakeHeaders ?? null);
219
+ if (this.requestTrace !== null) closeRequestTrace(this, status, this.error);
220
+ return true;
221
+ }
144
222
  async connect() {
145
223
  if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
146
224
  if (this.rejected) throw new Error("Nodefony Websocket rejected");
@@ -152,6 +230,7 @@ var WebsocketContext = class extends Context {
152
230
  this.requestEnded = true;
153
231
  this.connection.on("message", AsyncResource.bind(this.handleMessage.bind(this)));
154
232
  this.logRequest(null, this.acceptedProtocol ?? null);
233
+ if (this.requestTrace !== null) closeRequestTrace(this, 101, null);
155
234
  this.webSocketState = "connected";
156
235
  return this.connection;
157
236
  }
@@ -261,10 +340,11 @@ var WebsocketContext = class extends Context {
261
340
  * zéro allocation dans ce cas.
262
341
  */
263
342
  beginFrame(method, framePath) {
264
- if (!this.profiling && !this.timingEnabled) return null;
343
+ const profiled = this.profiling && this.httpKernel?.shouldProfile(framePath) === true;
344
+ if (!profiled && !this.timingEnabled) return null;
265
345
  this.#frameSeq += 1;
266
346
  return new FrameProfile({
267
- requestId: `${this.requestId}.${this.#frameSeq}`,
347
+ requestId: frameRequestId(this.requestId, this.#frameSeq),
268
348
  type: this.type,
269
349
  scheme: this.scheme,
270
350
  method,
@@ -274,7 +354,7 @@ var WebsocketContext = class extends Context {
274
354
  security: this.security ?? null,
275
355
  securityTrace: this.securityTrace,
276
356
  timing: this.timingEnabled,
277
- queries: this.profiling
357
+ queries: profiled
278
358
  });
279
359
  }
280
360
  /**
@@ -282,7 +362,7 @@ var WebsocketContext = class extends Context {
282
362
  * Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
283
363
  */
284
364
  collectFrame(frame) {
285
- if (frame === null || !this.profiling) return;
365
+ if (frame === null || !frame.profiled) return;
286
366
  this.get("profiler")?.collect(frame);
287
367
  }
288
368
  /**
@@ -337,12 +417,12 @@ var WebsocketContext = class extends Context {
337
417
  }
338
418
  onClose(code, reason) {
339
419
  const description = reason.toString();
340
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.type} ${logColor.magenta(code)} CLOSE ${this.method}`);
420
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.serverType} ${logColor.magenta(code)} CLOSE ${this.method}`);
341
421
  if (this.connection?.readyState !== Ws.CLOSED) {
342
422
  try {
343
423
  this.response?.drop(code, description);
344
424
  } catch (e) {
345
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.type} CLOSE ${logColor.red(this.method ?? "")}`);
425
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.serverType} CLOSE ${logColor.red(this.method ?? "")}`);
346
426
  }
347
427
  this.fire("onClose", code, description, this.connection);
348
428
  } else this.fire("onClose", code, description, this.connection);
@@ -359,7 +439,7 @@ var WebsocketContext = class extends Context {
359
439
  */
360
440
  onConnectionError(error) {
361
441
  this.webSocketState = "error";
362
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
442
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.serverType} ${logColor.red("SOCKET ERROR")} ${this.method}`);
363
443
  this.fire("onError", error, this);
364
444
  }
365
445
  setScheme() {
@@ -417,5 +497,4 @@ var WebsocketContext = class extends Context {
417
497
  this.rejected = true;
418
498
  }
419
499
  };
420
- //#endregion
421
- export { WebsocketContext as default, toWsCloseCode };
500
+ export { WebsocketContext as default, handshakeRefusalStatus, toHandshakeStatus, toWsCloseCode };
@@ -0,0 +1,17 @@
1
+ // nodefony/src/context/websocket/isWebsocketContext.ts
2
+ /**
3
+ * Dit si un contexte sert une connexion WebSocket — la garde qui rétrécit
4
+ * `ContextType` à `WebsocketContext` sans conversion.
5
+ *
6
+ * Elle lit la famille (`type`) posée à la construction du contexte, jamais
7
+ * `instanceof` : deux copies installées de `@nodefony/http` font échouer
8
+ * `instanceof` en silence, quand ce champ dit vrai pour les deux. Deux
9
+ * Une comparaison de chaînes, aucune allocation.
10
+ *
11
+ * @param context - le contexte à éprouver.
12
+ * @returns `true` si le contexte traite une connexion WebSocket (`ws` ou `wss`).
13
+ */
14
+ function isWebsocketContext(context) {
15
+ return context.type === "websocket";
16
+ }
17
+ export { isWebsocketContext };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/websocketUrl.ts
1
+ // nodefony/src/context/websocket/websocketUrl.ts
2
2
  /**
3
3
  * URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
4
4
  *
@@ -17,5 +17,4 @@
17
17
  function parseWebsocketUrl(scheme, host, rawUrl) {
18
18
  return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
19
19
  }
20
- //#endregion
21
20
  export { parseWebsocketUrl };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsBackpressure.ts
1
+ // nodefony/src/context/websocket/wsBackpressure.ts
2
2
  /**
3
3
  * Relit le seuil + la politique depuis les options du `WebSocketServer`.
4
4
  *
@@ -52,5 +52,4 @@ const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
52
52
  }
53
53
  return "drop";
54
54
  };
55
- //#endregion
56
55
  export { decideSend, readBackpressureOptions };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsCloseReason.ts
1
+ // nodefony/src/context/websocket/wsCloseReason.ts
2
2
  /**
3
3
  * Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
4
4
  * §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
@@ -37,5 +37,4 @@ function toWsCloseReason(reason) {
37
37
  }
38
38
  return reason.slice(0, end) + ELLIPSIS;
39
39
  }
40
- //#endregion
41
40
  export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/websocket/wsLogContent.ts
1
+ // nodefony/src/context/websocket/wsLogContent.ts
2
2
  /**
3
3
  * Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
4
4
  * le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
@@ -64,5 +64,4 @@ function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
64
64
  if (typeof s !== "string") return String(data);
65
65
  return s.length > cap ? `${s.slice(0, cap)}…` : s;
66
66
  }
67
- //#endregion
68
67
  export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
@@ -2,7 +2,7 @@ import { extend } from "nodefony";
2
2
  import crypto from "node:crypto";
3
3
  import { parseCookie } from "cookie";
4
4
  import MS from "ms";
5
- //#region nodefony/src/cookies/cookie.ts
5
+ // nodefony/src/cookies/cookie.ts
6
6
  const encode = encodeURIComponent;
7
7
  const decode = decodeURIComponent;
8
8
  const cookieDefaultSettings = {
@@ -20,11 +20,9 @@ function parser(strToParse) {
20
20
  }
21
21
  function cookiesParser(context) {
22
22
  let cookies = null;
23
- let co = null;
23
+ let co;
24
24
  switch (context.type) {
25
25
  case "http":
26
- case "https":
27
- case "http2":
28
26
  if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
29
27
  if (cookies) {
30
28
  const obj = parser(cookies);
@@ -35,8 +33,7 @@ function cookiesParser(context) {
35
33
  if (context.response) context.request.request.cookies = context.response.cookies;
36
34
  }
37
35
  break;
38
- case "websocket":
39
- case "websocket-secure": {
36
+ case "websocket": {
40
37
  const cookieHeader = context.request?.headers.cookie;
41
38
  if (cookieHeader) {
42
39
  const parsed = parser(cookieHeader);
@@ -254,5 +251,4 @@ var Cookie = class Cookie {
254
251
  return obj;
255
252
  }
256
253
  };
257
- //#endregion
258
254
  export { cookiesParser, Cookie as default, parser };
@@ -1,5 +1,5 @@
1
1
  import { clc, nodefonyError } from "nodefony";
2
- //#region nodefony/src/errors/httpError.ts
2
+ // nodefony/src/errors/httpError.ts
3
3
  const exclude = {
4
4
  context: true,
5
5
  resolver: true,
@@ -65,5 +65,4 @@ var HttpError = class extends nodefonyError {
65
65
  }
66
66
  };
67
67
  Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
68
- //#endregion
69
68
  export { HttpError as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/errors/thrownMessage.ts
1
+ // nodefony/src/errors/thrownMessage.ts
2
2
  /**
3
3
  * Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
4
4
  *
@@ -23,5 +23,4 @@ function thrownMessage(value) {
23
23
  case "function": return value === null ? "null" : Object.prototype.toString.call(value);
24
24
  }
25
25
  }
26
- //#endregion
27
26
  export { thrownMessage };
@@ -1,5 +1,5 @@
1
1
  import { thrownMessage } from "../errors/thrownMessage.js";
2
- //#region nodefony/src/profiler/FrameProfile.ts
2
+ // nodefony/src/profiler/FrameProfile.ts
3
3
  /**
4
4
  * Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
5
5
  * la connexion qui la porte.
@@ -37,12 +37,22 @@ var FrameProfile = class {
37
37
  * invocation, il redevient exact, et le SQL se replace dans le waterfall.
38
38
  */
39
39
  profilerQueries;
40
+ /**
41
+ * Ce profil sera-t-il gardé par le Profiler ? Faux quand seul le timing
42
+ * l'a ouvert (chemin d'outillage) : son id ne désigne alors aucun profil
43
+ * lisible, et ne doit pas être annoncé au client.
44
+ */
45
+ get profiled() {
46
+ return this.profilerQueries !== null;
47
+ }
40
48
  /** Resolver de la frame — porte route / controller / action. */
41
49
  resolver = null;
42
50
  /** Statut HTTP-équivalent de l'invocation (200, 403, 404…). */
43
51
  response = null;
44
52
  error = null;
45
53
  timing;
54
+ clockStartMs;
55
+ durationMs = null;
46
56
  constructor(init) {
47
57
  this.requestId = init.requestId;
48
58
  this.type = init.type;
@@ -54,9 +64,20 @@ var FrameProfile = class {
54
64
  this.security = init.security;
55
65
  this.securityTrace = init.securityTrace;
56
66
  this.timing = init.timing;
67
+ this.clockStartMs = init.timing ? performance.now() : 0;
57
68
  this.profilerQueries = init.queries ? [] : null;
58
69
  }
59
70
  /**
71
+ * Arrête l'horloge de la frame (au premier appel) et rend sa durée en ms ;
72
+ * `null` si le timing est éteint. Contrat de `IContext.stopClock`.
73
+ *
74
+ * @returns la durée de l'invocation, ou `null`
75
+ */
76
+ stopClock() {
77
+ if (!this.timing) return null;
78
+ return this.durationMs ??= performance.now() - this.clockStartMs;
79
+ }
80
+ /**
60
81
  * Ouvre une phase. Contrairement au `Context`, l'index est un **scan arrière**
61
82
  * (pas de `Map<nom, index>`) : une frame porte une poignée de phases, et une
62
83
  * phase peut être ré-entrante (une action qui en déclenche une autre) — la
@@ -88,9 +109,9 @@ var FrameProfile = class {
88
109
  * @param error - l'erreur qui a interrompu la frame, le cas échéant.
89
110
  */
90
111
  finish(status, error) {
112
+ this.stopClock();
91
113
  this.response = { statusCode: status };
92
114
  if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
93
115
  }
94
116
  };
95
- //#endregion
96
117
  export { FrameProfile, FrameProfile as default };
@@ -1,24 +1,47 @@
1
- //#region nodefony/src/profiler/Profiler.ts
2
- /** Durée totale = (fin de la dernière phase terminée) − (début de la 1ère). */
3
- function totalDuration(phases) {
4
- if (phases.length === 0) return null;
5
- const first = phases.at(0);
6
- if (first === void 0 || typeof first.startMs !== "number") return null;
7
- let end = first.startMs;
8
- for (const p of phases) {
9
- const e = p.startMs + (p.durationMs ?? 0);
10
- if (e > end) end = e;
11
- }
12
- const d = end - first.startMs;
13
- return d >= 0 ? Math.round(d * 1e3) / 1e3 : null;
1
+ import { profileLane } from "./aggregate.js";
2
+ import { frameIndexOf } from "nodefony";
3
+ // nodefony/src/profiler/Profiler.ts
4
+ /** Arrondit une durée à la microseconde (affichage du Suivi). */
5
+ function roundMicro(ms) {
6
+ return ms === null ? null : Math.round(ms * 1e3) / 1e3;
14
7
  }
15
8
  /** Capacité par défaut du ring buffer (assez pour une session de debug). */
16
9
  const DEFAULT_CAP = 500;
10
+ /**
11
+ * Capacité par défaut des deux anneaux annexes : l'OUTILLAGE (console, API
12
+ * d'administration, porte MCP) et les ASSETS relayés de Vite. Un chargement
13
+ * de la console relaie à lui seul quelques centaines de modules : dans un
14
+ * tampon commun, il chassait en quelques secondes les profils de
15
+ * l'application, puis ceux des appels de la console elle-même.
16
+ */
17
+ const DEFAULT_SIDE_CAP = 100;
18
+ /**
19
+ * Collecteur des profils de requête, en un anneau par provenance
20
+ * ({@link profileLane}) : l'application, l'outillage, les assets relayés.
21
+ * Chacun évince ses propres profils — ouvrir la console ne chasse plus celui
22
+ * qu'on y consulte.
23
+ */
17
24
  var Profiler = class {
18
- _buf = /* @__PURE__ */ new Map();
19
- _cap;
20
- constructor(cap = DEFAULT_CAP) {
21
- this._cap = cap > 0 ? cap : DEFAULT_CAP;
25
+ _lanes = {
26
+ app: /* @__PURE__ */ new Map(),
27
+ tooling: /* @__PURE__ */ new Map(),
28
+ asset: /* @__PURE__ */ new Map()
29
+ };
30
+ _caps;
31
+ /** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
32
+ _seq = 0;
33
+ /**
34
+ * @param cap - capacité de l'anneau de l'application.
35
+ * @param sideCap - capacité de chacun des anneaux de l'outillage et des
36
+ * assets relayés.
37
+ */
38
+ constructor(cap = DEFAULT_CAP, sideCap = DEFAULT_SIDE_CAP) {
39
+ const side = sideCap > 0 ? sideCap : DEFAULT_SIDE_CAP;
40
+ this._caps = {
41
+ app: cap > 0 ? cap : DEFAULT_CAP,
42
+ tooling: side,
43
+ asset: side
44
+ };
22
45
  }
23
46
  /**
24
47
  * Snapshot un `Context` en fin de requête. Tolérant aux champs absents
@@ -32,13 +55,14 @@ var Profiler = class {
32
55
  const phases = ctx.phases ?? [];
33
56
  const entry = {
34
57
  requestId,
58
+ seq: ++this._seq,
35
59
  ts: Date.now(),
36
- kind: ctx.type === "websocket" || ctx.type === "websocket-secure" ? "ws" : "http",
60
+ kind: ctx.type === "websocket" ? "ws" : "http",
37
61
  method: ctx.method ?? null,
38
62
  url: ctx.url ?? "",
39
63
  scheme: ctx.scheme ?? "",
40
64
  status: ctx.response?.statusCode ?? (ctx.error ? 500 : null),
41
- durationMs: totalDuration(phases),
65
+ durationMs: roundMicro(ctx.stopClock()),
42
66
  route: ctx.resolver?.route?.name ?? null,
43
67
  controller: ctx.resolver?.controller?.name ?? null,
44
68
  action: ctx.resolver?.actionName ?? null,
@@ -54,25 +78,36 @@ var Profiler = class {
54
78
  queries: ctx.profilerQueries && ctx.profilerQueries.length > 0 ? ctx.profilerQueries : void 0,
55
79
  security: readSecurity(ctx)
56
80
  };
57
- this._buf.delete(requestId);
58
- this._buf.set(requestId, entry);
59
- if (this._buf.size > this._cap) {
60
- const oldest = this._buf.keys().next().value;
61
- if (oldest !== void 0) this._buf.delete(oldest);
81
+ const { app, tooling, asset } = this._lanes;
82
+ app.delete(requestId);
83
+ tooling.delete(requestId);
84
+ asset.delete(requestId);
85
+ const lane = profileLane(entry.url);
86
+ const buf = this._lanes[lane];
87
+ buf.set(requestId, entry);
88
+ if (buf.size > this._caps[lane]) {
89
+ const oldest = buf.keys().next().value;
90
+ if (oldest !== void 0) buf.delete(oldest);
62
91
  }
63
92
  }
64
93
  /** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
65
94
  get(requestId) {
66
- return this._buf.get(requestId);
95
+ const { app, tooling, asset } = this._lanes;
96
+ return app.get(requestId) ?? tooling.get(requestId) ?? asset.get(requestId);
67
97
  }
68
98
  /**
69
99
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
100
+ *
101
+ * @param limit - nombre maximal de résumés
102
+ * @param connection - le requestId d'une connexion WebSocket : ses SEULES
103
+ * frames du pont (`<connection>.<n>`, règle `frameIndexOf` du cœur)
70
104
  */
71
- recent(limit = 60) {
105
+ recent(limit = 60, connection) {
72
106
  const out = [];
73
- const entries = [...this._buf.values()];
74
- for (const e of entries.reverse()) {
107
+ const entries = this.#collectAfter(0).reverse();
108
+ for (const e of entries) {
75
109
  if (out.length >= limit) break;
110
+ if (connection && frameIndexOf(e.requestId, connection) === null) continue;
76
111
  out.push({
77
112
  requestId: e.requestId,
78
113
  ts: e.ts,
@@ -87,13 +122,51 @@ var Profiler = class {
87
122
  }
88
123
  return out;
89
124
  }
125
+ /** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
126
+ get cursor() {
127
+ return this._seq;
128
+ }
129
+ /**
130
+ * Profils collectés après `cursor`, tous anneaux confondus, dans l'ordre
131
+ * des numéros.
132
+ *
133
+ * Les pertes se comptent sans estimation : chaque numéro attribué est un
134
+ * profil collecté, donc ce qui manque entre le curseur et le dernier numéro
135
+ * a disparu avant d'être lu. Un curseur venu d'ailleurs (supérieur au
136
+ * dernier numéro) repart de zéro.
137
+ *
138
+ * @param cursor - le `cursor` rendu par l'appel précédent, ou {@link cursor}.
139
+ */
140
+ since(cursor) {
141
+ const from = cursor >= 0 && cursor <= this._seq ? cursor : 0;
142
+ const entries = this.#collectAfter(from);
143
+ return {
144
+ entries,
145
+ cursor: this._seq,
146
+ lost: this._seq - from - entries.length
147
+ };
148
+ }
90
149
  /** Nombre de profils en mémoire. */
91
150
  get size() {
92
- return this._buf.size;
151
+ const { app, tooling, asset } = this._lanes;
152
+ return app.size + tooling.size + asset.size;
93
153
  }
94
- /** Vide le ring buffer. */
154
+ /** Vide les anneaux. */
95
155
  clear() {
96
- this._buf.clear();
156
+ const { app, tooling, asset } = this._lanes;
157
+ app.clear();
158
+ tooling.clear();
159
+ asset.clear();
160
+ }
161
+ /**
162
+ * Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
163
+ * ancien au plus récent. Lecture à la demande (console, outil de l'agent) :
164
+ * jamais sur le chemin d'une requête.
165
+ */
166
+ #collectAfter(from) {
167
+ const out = [];
168
+ for (const lane of Object.values(this._lanes)) for (const e of lane.values()) if (e.seq > from) out.push(e);
169
+ return out.sort((a, b) => a.seq - b.seq);
97
170
  }
98
171
  };
99
172
  /**
@@ -131,5 +204,4 @@ function readSecurity(ctx) {
131
204
  function readUser(ctx) {
132
205
  return ctx.userIdentifier ?? ctx.securityTrace?.user ?? null;
133
206
  }
134
- //#endregion
135
207
  export { Profiler, Profiler as default };