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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +22 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -0,0 +1,53 @@
1
+ import type { AlpnProtocol, ProtocolName, ServerType } from "../../interfaces/IContext.js";
2
+ /**
3
+ * Famille du protocole traité par un contexte, déduite du serveur qui l'a
4
+ * reçu. Règle UNIQUE : `Context` la pose à la construction.
5
+ *
6
+ * @param serverType - identité du serveur (`http`, `https`, `ws`, `wss`)
7
+ * @returns `"websocket"` pour `ws`/`wss`, `"http"` sinon
8
+ */
9
+ export declare function protocolNameOf(serverType: ServerType): ProtocolName;
10
+ /**
11
+ * Ce que la détection lit d'une requête Node, et rien d'autre : `http.IncomingMessage`
12
+ * (HTTP/1.1) et `http2.Http2ServerRequest` (h2, h2c) le satisfont par leur forme, sans
13
+ * transtypage — et une doublure de test s'écrit en littéral.
14
+ */
15
+ export interface IAlpnSource {
16
+ readonly httpVersionMajor: number;
17
+ readonly stream?: {
18
+ readonly session?: {
19
+ readonly alpnProtocol?: string | undefined;
20
+ } | undefined;
21
+ } | undefined;
22
+ }
23
+ /**
24
+ * Protocole du saut qui a porté cette requête jusqu'au processus, au sens du
25
+ * registre IANA des identifiants ALPN. Règle UNIQUE : `HttpContext` la lit pour
26
+ * choisir ses classes de requête et de réponse.
27
+ *
28
+ * La valeur se CONSTATE sur la requête, elle ne se déduit pas du serveur : un
29
+ * même serveur TLS reçoit HTTP/1.1 et h2 (`allowHTTP1`). Pour h2, c'est la
30
+ * session de Node qui tranche — `Http2Session.alpnProtocol` vaut `"h2c"` sur
31
+ * une session en clair, l'ALPN négocié (`"h2"`) sur une session TLS.
32
+ *
33
+ * @param request - la requête Node reçue par le serveur
34
+ * @returns `"http/1.1"`, `"h2"` ou `"h2c"`
35
+ */
36
+ export declare function alpnProtocolOf(request: IAlpnSource): AlpnProtocol;
37
+ /**
38
+ * Autorité (hôte et port) d'une requête, HTTP/1.1 comme HTTP/2. Règle UNIQUE
39
+ * des lecteurs du serveur.
40
+ *
41
+ * `:authority` d'abord, puis `Host` (RFC 9113 §8.3.1). Le repli n'est pas un
42
+ * confort : un intermédiaire qui convertit une requête HTTP/1.1 de forme origine
43
+ * (`GET /chemin`) en HTTP/2 ne DOIT PAS fabriquer `:authority` — il garde
44
+ * `Host`. C'est ce que fait HAProxy `proto h2` vers un port h2c : sans ce repli,
45
+ * l'hôte est introuvable et la requête part en 421.
46
+ *
47
+ * @param headers - en-têtes de la requête Node (`IncomingMessage` ou `Http2ServerRequest`).
48
+ * @returns l'autorité, ou `undefined` si la requête n'en porte aucune.
49
+ */
50
+ export declare function requestAuthority(headers: {
51
+ host?: string | undefined;
52
+ ":authority"?: string | undefined;
53
+ }): string | undefined;
@@ -1,3 +1,4 @@
1
+ import type { OutgoingHttpHeaders } from "node:http";
1
2
  import Cookie from "../../cookies/cookie.js";
2
3
  import { Message, Msgid, Pci, Severity } from "nodefony";
3
4
  import WebsocketContext from "./WebsocketContext.js";
@@ -44,8 +45,23 @@ declare class WebsocketResponse {
44
45
  };
45
46
  clean(): void;
46
47
  setEncoding(encoding: BufferEncoding): BufferEncoding;
47
- setHeader(): boolean;
48
- setHeaders(): boolean;
48
+ /**
49
+ * En-têtes de la réponse au handshake, retenus tant que la socket n'est pas
50
+ * acceptée : ils ne partent que dans un REFUS (`refuseHandshake`) — le `101`
51
+ * de `ws` ne les porte pas. `null` tant que personne n'en pose (le cas
52
+ * nominal : aucune allocation).
53
+ */
54
+ handshakeHeaders: Record<string, string | number> | null;
55
+ /**
56
+ * Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
57
+ * une fois la socket ouverte, où il n'y a plus de réponse HTTP.
58
+ *
59
+ * @param name - nom de l'en-tête
60
+ * @param value - valeur
61
+ * @returns toujours `true`
62
+ */
63
+ setHeader(name: string, value: number | string | readonly string[]): boolean;
64
+ setHeaders(obj: OutgoingHttpHeaders): boolean;
49
65
  addCookie(cookie: Cookie): void;
50
66
  setCookies(): void;
51
67
  setCookie(_cookie: Cookie): void;
@@ -4,21 +4,21 @@ import { Severity, Msgid, Message, nodefonyError, Scope } from "nodefony";
4
4
  import Ws from "ws";
5
5
  import type { IncomingMessage } from "node:http";
6
6
  import WebsocketResponse from "./Response.js";
7
+ import type { IUpgradeAdmission } from "../../servers/upgradeDispatch.js";
7
8
  import { URL } from "node:url";
8
9
  import { HTTPMethod } from "../Context.js";
9
10
  import HttpError from "../../errors/httpError.js";
10
11
  import { FrameProfile } from "../../profiler/FrameProfile.js";
11
- import type { PhaseName } from "../../../interfaces/IContext.js";
12
+ import type { PhaseName, IWebsocketContext } from "../../../interfaces/IContext.js";
12
13
  import { ProxyType } from "../http/HttpContext.js";
13
14
  import { type ResolvedProxy } from "../forwarded.js";
14
15
  export interface IWsRequestExtension {
15
16
  url: URL;
16
17
  query: Record<string, string>;
17
- queryGet: Record<string, string>;
18
18
  path: string;
19
19
  }
20
20
  export type WsIncomingMessage = IncomingMessage & IWsRequestExtension;
21
- import type { IWebsocketContext as IWebsocketContextInterface } from "../../../interfaces/IContext.js";
21
+ import type { IWebsocketContext as IWebsocketContextInterface, IWebsocketConnectionState } from "../../../interfaces/IContext.js";
22
22
  /**
23
23
  * Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
24
24
  * et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
@@ -36,7 +36,36 @@ import type { IWebsocketContext as IWebsocketContextInterface } from "../../../i
36
36
  * @returns un code de fermeture WS valide.
37
37
  */
38
38
  export declare function toWsCloseCode(code: number | undefined | null): number;
39
- export default class WebsocketContext extends Context implements IWebsocketContextInterface {
39
+ /**
40
+ * Statut HTTP d'un handshake REFUSÉ avant le `101` (RFC 6455 §4.2.2) — la
41
+ * réciproque de {@link toWsCloseCode} : une erreur levée pendant la décision
42
+ * porte un code HTTP (pare-feu, garde) ou, par héritage, un code de fermeture
43
+ * WS (origine `1008`, protocole `1002`, introuvable `4004`) ; le client qui
44
+ * lit le statut doit y reconnaître le même sens.
45
+ *
46
+ * @param code - code porté par l'erreur
47
+ * @returns un statut HTTP 4xx/5xx
48
+ */
49
+ export declare function toHandshakeStatus(code: number | undefined | null): number;
50
+ /**
51
+ * Statut HTTP que reçoit le client d'un handshake refusé — ou `null` si la
52
+ * connexion était déjà acceptée.
53
+ *
54
+ * La ligne de journal d'un refus s'écrit AVANT `refuseHandshake` : sans ce
55
+ * calcul, elle affichait le code de fermeture par défaut (`1000`) en ERROR,
56
+ * pour un `400` que le client reçoit. Même conversion que le refus
57
+ * ({@link toHandshakeStatus}), donc même chiffre. Chemin d'ERREUR seulement :
58
+ * 1 comparaison et 1 `typeof`, aucune allocation.
59
+ *
60
+ * @param context - le contexte WS de la ligne
61
+ * @param error - l'erreur qui a interrompu le handshake
62
+ * @returns le statut HTTP du refus, ou `null` après l'acceptation
63
+ */
64
+ export declare function handshakeRefusalStatus(context: IWebsocketContext, error: Error): number | null;
65
+ interface WebsocketContext extends IWebsocketConnectionState {
66
+ }
67
+ export { WebsocketContext as default };
68
+ declare class WebsocketContext extends Context implements IWebsocketContextInterface {
40
69
  #private;
41
70
  request: WsIncomingMessage | null;
42
71
  response: WebsocketResponse | null;
@@ -45,16 +74,37 @@ export default class WebsocketContext extends Context implements IWebsocketConte
45
74
  rejected: boolean;
46
75
  teardownWired: boolean;
47
76
  connection: Ws | null;
77
+ /**
78
+ * Le verdict du handshake encore à rendre : posé par le noyau tant que le
79
+ * pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
80
+ * est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
81
+ */
82
+ admission: IUpgradeAdmission | null;
48
83
  origin: string;
49
84
  proxy: ProxyType | null;
50
85
  forwarded: ResolvedProxy | null;
51
86
  wsUrl: URL | null;
52
- queryGet: Record<string, string>;
53
- queryRequest: Record<string, string>;
87
+ query: Record<string, string>;
54
88
  wsPath: string;
55
- constructor(scope: Scope, req: IncomingMessage, ws: Ws, type: ServerType);
89
+ constructor(scope: Scope, req: IncomingMessage, ws: Ws | null, serverType: ServerType);
56
90
  log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): import("nodefony").Pdu;
57
91
  logRequest(httpError?: Error | HttpError | nodefonyError | null, acceptedProtocol?: string | null): import("nodefony").Pdu | undefined;
92
+ /**
93
+ * Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
94
+ * contexte a été construit et jugé AVANT, sans elle.
95
+ *
96
+ * @param ws - la socket acceptée
97
+ */
98
+ acceptConnection(ws: Ws): void;
99
+ /**
100
+ * Refuse le handshake par un statut HTTP, avant tout `101` (RFC 6455
101
+ * §4.2.2) — avec les en-têtes posés pendant la décision (`WWW-Authenticate`
102
+ * du pare-feu, `Retry-After`).
103
+ *
104
+ * @param status - statut HTTP du refus
105
+ * @returns `false` si le verdict était déjà rendu (rien n'est écrit)
106
+ */
107
+ refuseHandshake(status: number): boolean;
58
108
  connect(): Promise<Ws>;
59
109
  handle(data?: unknown[]): Promise<this>;
60
110
  render(chunk: unknown, encoding?: BufferEncoding): Promise<WebsocketResponse>;
@@ -0,0 +1,15 @@
1
+ import type { ContextType } from "../../../service/http-kernel.js";
2
+ import type WebsocketContext from "./WebsocketContext.js";
3
+ /**
4
+ * Dit si un contexte sert une connexion WebSocket — la garde qui rétrécit
5
+ * `ContextType` à `WebsocketContext` sans conversion.
6
+ *
7
+ * Elle lit la famille (`type`) posée à la construction du contexte, jamais
8
+ * `instanceof` : deux copies installées de `@nodefony/http` font échouer
9
+ * `instanceof` en silence, quand ce champ dit vrai pour les deux. Deux
10
+ * Une comparaison de chaînes, aucune allocation.
11
+ *
12
+ * @param context - le contexte à éprouver.
13
+ * @returns `true` si le contexte traite une connexion WebSocket (`ws` ou `wss`).
14
+ */
15
+ export declare function isWebsocketContext(context: ContextType): context is WebsocketContext;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
3
+ *
4
+ * Seule construction : le contexte WebSocket en tire son chemin et sa requête,
5
+ * et le contrôle d'avant `101` en tire le chemin qu'il confronte aux routes.
6
+ * Deux constructions divergeraient — `//a/b` lu comme un chemin d'un côté, comme
7
+ * un hôte de l'autre — et une route acceptée par l'une serait refusée par
8
+ * l'autre.
9
+ *
10
+ * @param scheme - `ws` ou `wss`
11
+ * @param host - en-tête `Host` de la requête (`localhost` s'il manque)
12
+ * @param rawUrl - cible brute de la requête (`/` si elle manque)
13
+ * @returns l'URL analysée
14
+ * @throws TypeError si l'hôte ou la cible ne forment pas une URL
15
+ */
16
+ export declare function parseWebsocketUrl(scheme: string, host: string | undefined, rawUrl: string | undefined): URL;
@@ -1,5 +1,6 @@
1
1
  import type { IProfilerQuery } from "nodefony";
2
- import type { ISecurityTrace, PhaseName, PhaseTiming } from "../../interfaces/IContext.js";
2
+ import type { ISecurityTrace, PhaseName, PhaseTiming, ProtocolName } from "../../interfaces/IContext.js";
3
+ import type { IResolvedRoute, IRouteResolver } from "../../interfaces/IRouting.js";
3
4
  /**
4
5
  * Zone firewall capturée sur le contexte (`SecuredArea`, lue en structurel :
5
6
  * `@nodefony/http` ne peut pas importer `@nodefony/security`).
@@ -10,22 +11,22 @@ export interface ProfiledArea {
10
11
  mode?: string;
11
12
  authenticators?: readonly string[];
12
13
  }
13
- /** Ce que le Profiler lit d'un Resolver (route/controller/action). */
14
+ /**
15
+ * Ce que le Profiler lit d'un Resolver (route/controller/action) — dérivé de
16
+ * {@link IRouteResolver}, en lecture seule : tout résolveur du contrat s'y range
17
+ * sans conversion, et les deux types ne peuvent plus diverger.
18
+ */
14
19
  export interface ProfiledResolver {
15
- route?: {
16
- name?: string;
17
- } | null;
18
- controller?: {
19
- name?: string;
20
- } | null;
21
- actionName?: string;
20
+ readonly route?: Pick<IResolvedRoute, "name"> | null | undefined;
21
+ readonly controller?: IRouteResolver["controller"];
22
+ readonly actionName?: string | undefined;
22
23
  }
23
24
  /** Descripteur d'ouverture d'une invocation (ce que le pont connaît d'entrée). */
24
25
  export interface FrameProfileInit {
25
26
  /** Identifiant du profil — `<requestId de la connexion>.<n° de frame>`. */
26
27
  requestId: string;
27
- /** Type de transport du contexte porteur (`websocket` / `websocket-secure`). */
28
- type: string;
28
+ /** Famille du contexte porteur (`context.type` : `websocket`). */
29
+ type: ProtocolName;
29
30
  scheme: string;
30
31
  /** Méthode LOGIQUE de l'invocation (`GET`, `POST`…), pas le transport. */
31
32
  method: string;
@@ -63,7 +64,7 @@ export interface FrameProfileInit {
63
64
  */
64
65
  export declare class FrameProfile {
65
66
  readonly requestId: string;
66
- readonly type: string;
67
+ readonly type: ProtocolName;
67
68
  readonly scheme: string;
68
69
  readonly method: string;
69
70
  readonly url: string;
@@ -18,6 +18,8 @@
18
18
  * Multi-process : un profil vit sur le worker qui a traité l'appel (fetch même
19
19
  * origine par requestId). Agrégat cluster = Redis (P13).
20
20
  */
21
+ import type { ProfiledResolver } from "./FrameProfile.js";
22
+ import type { ProtocolName } from "../../interfaces/IContext.js";
21
23
  /** Une phase du pipeline, projetée pour le transport (durée résolue). */
22
24
  export interface ProfilePhase {
23
25
  name: string;
@@ -77,6 +79,11 @@ export interface ProfileSecurity {
77
79
  /** Profil complet d'une requête (HTTP ou message WS), exposé par `get`. */
78
80
  export interface ProfileEntry {
79
81
  requestId: string;
82
+ /**
83
+ * Numéro d'ordre de collecte, croissant sur la vie du profileur. Un profil
84
+ * réinséré (identifiant réutilisé) reçoit un numéro neuf.
85
+ */
86
+ seq: number;
80
87
  /** Horodatage de la collecte (`Date.now()`), pour le tri client. */
81
88
  ts: number;
82
89
  kind: "http" | "ws";
@@ -116,7 +123,8 @@ export interface ProfileSummary {
116
123
  /** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
117
124
  interface ProfilableContext {
118
125
  requestId?: string | undefined;
119
- type?: string | undefined;
126
+ userIdentifier?: string | null | undefined;
127
+ type?: ProtocolName | undefined;
120
128
  scheme?: string | undefined;
121
129
  method?: string | null | undefined;
122
130
  url?: string | undefined;
@@ -133,15 +141,7 @@ interface ProfilableContext {
133
141
  startMs: number;
134
142
  durationMs?: number | undefined;
135
143
  }> | undefined;
136
- resolver?: {
137
- route?: {
138
- name?: string | undefined;
139
- } | null;
140
- controller?: {
141
- name?: string | undefined;
142
- } | null | undefined;
143
- actionName?: string | undefined;
144
- } | null | undefined;
144
+ resolver?: ProfiledResolver | null | undefined;
145
145
  /**
146
146
  * Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
147
147
  * que la payload ALS — les adapters ORM y poussent via `RequestContext`.
@@ -167,10 +167,50 @@ interface ProfilableContext {
167
167
  roles: string[] | null;
168
168
  } | null | undefined;
169
169
  }
170
- export declare class Profiler {
171
- private readonly _buf;
172
- private readonly _cap;
173
- constructor(cap?: number);
170
+ /** Les profils collectés depuis un curseur. */
171
+ export interface IProfileBatch {
172
+ /** Profils de numéro supérieur au curseur, du plus ancien au plus récent. */
173
+ entries: ProfileEntry[];
174
+ /** Curseur à repasser au prochain appel : le dernier numéro attribué. */
175
+ cursor: number;
176
+ /**
177
+ * Profils collectés depuis le curseur mais disparus avant d'être lus —
178
+ * évincés par le tampon, ou écrasés par un identifiant réutilisé. Exact.
179
+ */
180
+ lost: number;
181
+ }
182
+ /**
183
+ * Ce qu'un lecteur extérieur (outil de l'agent de code, banc) attend du
184
+ * profileur : suivre les profils au fil de l'eau, ou en retrouver un par son
185
+ * identifiant. Contrat structurel — on le résout par NOM dans le conteneur
186
+ * (`"profiler"`), sans dépendre de la classe.
187
+ */
188
+ export interface IProfilerReader {
189
+ /** Curseur courant : le passer à {@link since} pour ne lire que la suite. */
190
+ readonly cursor: number;
191
+ /** Profils collectés depuis `cursor`, et le compte de ceux qui ont été perdus. */
192
+ since(cursor: number): IProfileBatch;
193
+ /** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
194
+ get(requestId: string): ProfileEntry | undefined;
195
+ }
196
+ /**
197
+ * Collecteur des profils de requête, en un anneau par provenance
198
+ * ({@link profileLane}) : l'application, l'outillage, les assets relayés.
199
+ * Chacun évince ses propres profils — ouvrir la console ne chasse plus celui
200
+ * qu'on y consulte.
201
+ */
202
+ export declare class Profiler implements IProfilerReader {
203
+ #private;
204
+ private readonly _lanes;
205
+ private readonly _caps;
206
+ /** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
207
+ private _seq;
208
+ /**
209
+ * @param cap - capacité de l'anneau de l'application.
210
+ * @param sideCap - capacité de chacun des anneaux de l'outillage et des
211
+ * assets relayés.
212
+ */
213
+ constructor(cap?: number, sideCap?: number);
174
214
  /**
175
215
  * Snapshot un `Context` en fin de requête. Tolérant aux champs absents
176
216
  * (handshake WS, erreur précoce). À appeler AVANT `context.clean()`.
@@ -184,9 +224,23 @@ export declare class Profiler {
184
224
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
185
225
  */
186
226
  recent(limit?: number): ProfileSummary[];
227
+ /** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
228
+ get cursor(): number;
229
+ /**
230
+ * Profils collectés après `cursor`, tous anneaux confondus, dans l'ordre
231
+ * des numéros.
232
+ *
233
+ * Les pertes se comptent sans estimation : chaque numéro attribué est un
234
+ * profil collecté, donc ce qui manque entre le curseur et le dernier numéro
235
+ * a disparu avant d'être lu. Un curseur venu d'ailleurs (supérieur au
236
+ * dernier numéro) repart de zéro.
237
+ *
238
+ * @param cursor - le `cursor` rendu par l'appel précédent, ou {@link cursor}.
239
+ */
240
+ since(cursor: number): IProfileBatch;
187
241
  /** Nombre de profils en mémoire. */
188
242
  get size(): number;
189
- /** Vide le ring buffer. */
243
+ /** Vide les anneaux. */
190
244
  clear(): void;
191
245
  }
192
246
  export default Profiler;
@@ -0,0 +1,215 @@
1
+ import type { ProfileEntry } from "./Profiler.js";
2
+ /**
3
+ * Agrégation des profils de requêtes — règle PURE, sans horloge ni I/O.
4
+ *
5
+ * Une seule implémentation pour tout ce qui résume le profileur : l'outil MCP
6
+ * qui observe l'application (`nodefony_observe`), celui qui tire lui-même des
7
+ * requêtes sur une route, et demain la console d'administration. Elle est
8
+ * bornée par construction : un échantillon de durées par route, un nombre
9
+ * maximal de formes SQL retenues, quelques exemples d'erreurs — observer une
10
+ * application chargée ne fait jamais grossir la mémoire avec la fenêtre.
11
+ */
12
+ /**
13
+ * Répétitions d'une même forme SQL, dans UNE requête, à partir desquelles on
14
+ * signale un N+1. En dessous, ce sont des boucles courtes légitimes (quelques
15
+ * lectures de configuration, une pagination) qu'on signalerait à tort.
16
+ */
17
+ export declare const N_PLUS_ONE_MIN_REPEATS = 5;
18
+ /** Durées retenues par route pour les percentiles (échantillonnage au-delà). */
19
+ export declare const ROUTE_SAMPLE_CAP = 256;
20
+ /** Formes SQL distinctes retenues ; les suivantes ne sont plus que comptées. */
21
+ export declare const SQL_SHAPE_CAP = 200;
22
+ /** Exemples d'erreurs retenus (statut ≥ 500 ou exception). */
23
+ export declare const ERROR_SAMPLE_CAP = 10;
24
+ /** Préfixe des routes de l'outillage (console, API d'administration, porte MCP). */
25
+ export declare const TOOLING_URL_PREFIX = "/nodefony/";
26
+ /**
27
+ * Provenance d'un profil : l'application, l'outillage (`/nodefony/…` :
28
+ * console, API d'administration, porte MCP), ou un asset relayé sous un
29
+ * préfixe de module (`/_vite/`).
30
+ */
31
+ export type ProfileLane = "app" | "tooling" | "asset";
32
+ /**
33
+ * Range un profil par provenance, d'après son URL. Seule règle du partage —
34
+ * les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
35
+ *
36
+ * @param url - URL du profil, absolue ou chemin.
37
+ * @returns la provenance du profil.
38
+ */
39
+ export declare function profileLane(url: string): ProfileLane;
40
+ /**
41
+ * Le profil vient-il de l'outillage (routes `/nodefony/…` ou assets relayés)
42
+ * plutôt que de l'application.
43
+ *
44
+ * @param url - URL du profil, absolue ou chemin.
45
+ */
46
+ export declare function isToolingUrl(url: string): boolean;
47
+ /** Groupe des requêtes qu'aucune route n'a servies (404, statique, sondes). */
48
+ export declare const NO_ROUTE = "(aucune route)";
49
+ /** Pseudo-phase : le temps de l'action hors phases imbriquées et hors SQL. */
50
+ export declare const ACTION_SELF_PHASE = "action.self";
51
+ /** Répartition des statuts HTTP d'une route. */
52
+ export interface IStatusBreakdown {
53
+ s2xx: number;
54
+ s3xx: number;
55
+ s4xx: number;
56
+ s5xx: number;
57
+ /** Dont 401 : la route exige une authentification que la requête n'avait pas. */
58
+ unauthorized: number;
59
+ /** Dont 403 : authentifiée, mais refusée. */
60
+ forbidden: number;
61
+ }
62
+ /** Temps cumulé d'une phase sur les requêtes d'une route. */
63
+ export interface IPhaseTotal {
64
+ totalMs: number;
65
+ /** Requêtes où la phase est apparue et s'est terminée. */
66
+ count: number;
67
+ }
68
+ /** Statistiques d'une route sur la fenêtre agrégée. */
69
+ export interface IRouteStats {
70
+ route: string;
71
+ kind: "http" | "ws";
72
+ controller: string | null;
73
+ action: string | null;
74
+ count: number;
75
+ /** Requêtes sans durée mesurée (mesure des phases désactivée, erreur précoce). */
76
+ untimed: number;
77
+ /**
78
+ * Requêtes SERVIES et chronométrées (statut < 400, sans exception) : la
79
+ * population de tout ce qui se dit « par requête » ci-dessous. Refus et
80
+ * erreurs se lisent dans `status` et `errors`.
81
+ */
82
+ served: number;
83
+ /** Percentiles et maximum, sur les requêtes servies. */
84
+ p50Ms: number | null;
85
+ p95Ms: number | null;
86
+ maxMs: number | null;
87
+ status: IStatusBreakdown;
88
+ /** Requêtes en erreur : statut ≥ 500 ou exception levée. */
89
+ errors: number;
90
+ /**
91
+ * Requêtes SQL/NoSQL des requêtes servies : total, maximum dans une
92
+ * requête, temps CUMULÉ (des requêtes parallèles s'y additionnent).
93
+ */
94
+ queries: {
95
+ total: number;
96
+ perRequestMax: number;
97
+ totalMs: number;
98
+ };
99
+ /** Temps par phase des requêtes servies, `action.self` compris. */
100
+ phases: Record<string, IPhaseTotal>;
101
+ }
102
+ /** Un N+1 : la même forme SQL répétée dans une requête. */
103
+ export interface INPlusOne {
104
+ route: string;
105
+ controller: string | null;
106
+ action: string | null;
107
+ shape: string;
108
+ /** Plus grand nombre de répétitions vu dans une requête. */
109
+ maxRepeats: number;
110
+ /** Requêtes touchées. */
111
+ requests: number;
112
+ /** Temps cumulé de ces répétitions. */
113
+ totalMs: number;
114
+ /** Une requête touchée, pour aller voir son profil complet. */
115
+ sampleRequestId: string;
116
+ }
117
+ /** Une forme SQL et son coût sur la fenêtre. */
118
+ export interface ISqlShapeStats {
119
+ shape: string;
120
+ connector: string | null;
121
+ count: number;
122
+ totalMs: number;
123
+ maxMs: number;
124
+ /** Routes qui l'ont émise (au plus cinq). */
125
+ routes: string[];
126
+ }
127
+ /** Un exemple d'erreur, de quoi la retrouver dans le journal. */
128
+ export interface IErrorSample {
129
+ route: string;
130
+ status: number | null;
131
+ error: string | null;
132
+ requestId: string;
133
+ }
134
+ /** Le rapport agrégé. */
135
+ export interface IProfileReport {
136
+ /** Requêtes agrégées (hors outillage filtré). */
137
+ requests: number;
138
+ http: number;
139
+ ws: number;
140
+ /** Requêtes de l'outillage écartées (`/nodefony/…`, `/_vite/…`). */
141
+ filteredTooling: number;
142
+ /** Requêtes sans durée mesurée. */
143
+ untimed: number;
144
+ /** Triées par p95 décroissant. */
145
+ routes: IRouteStats[];
146
+ /** Triés par temps cumulé décroissant. */
147
+ nPlusOne: INPlusOne[];
148
+ /** Les dix formes SQL les plus coûteuses (temps cumulé décroissant). */
149
+ slowQueries: ISqlShapeStats[];
150
+ /** Formes SQL distinctes vues au-delà de la capacité, comptées sans détail. */
151
+ sqlShapesDropped: number;
152
+ errors: IErrorSample[];
153
+ }
154
+ /** Réglages d'une agrégation. */
155
+ export interface IProfileAggregatorOptions {
156
+ /** Répétitions à partir desquelles une forme SQL est un N+1. */
157
+ nPlusOneMinRepeats?: number | undefined;
158
+ /** Agréger aussi les requêtes de l'outillage (`/nodefony/…`, `/_vite/…`). */
159
+ includeTooling?: boolean | undefined;
160
+ /** Source d'aléa de l'échantillonnage — injectée par les tests. */
161
+ random?: (() => number) | undefined;
162
+ }
163
+ /** Accumulateur : on lui donne les profils un à un, il rend le rapport. */
164
+ export interface IProfileAggregator {
165
+ /** Ajoute un profil ; `false` s'il a été écarté (outillage). */
166
+ add(entry: ProfileEntry): boolean;
167
+ /** Le rapport à cet instant ; l'accumulateur continue d'accepter des profils. */
168
+ result(): IProfileReport;
169
+ }
170
+ /**
171
+ * Réduit une requête SQL (ou le descripteur d'une commande NoSQL) à sa FORME :
172
+ * valeurs remplacées par `?`, listes de valeurs repliées, blancs compactés.
173
+ *
174
+ * Deux requêtes qui ne diffèrent que par leurs valeurs ont la même forme — c'est
175
+ * ce qui permet de reconnaître un N+1. Et aucune valeur ne survit : la forme
176
+ * peut sortir dans un rapport sans emporter une donnée personnelle.
177
+ *
178
+ * @param sql - le texte tel que l'adaptateur l'a poussé au profileur.
179
+ * @returns la forme normalisée.
180
+ */
181
+ export declare function sqlShape(sql: string): string;
182
+ /**
183
+ * Une requête en ERREUR du serveur : statut ≥ 500, ou exception levée sans
184
+ * verdict client. Règle unique — le rapport, ses exemples et la lecture de leurs
185
+ * journaux en dépendent.
186
+ *
187
+ * Un statut 4xx n'en est JAMAIS une, même porteur d'une exception : un refus
188
+ * 401/403 (l'authentification lève une `AuthenticationError`) ou une ressource
189
+ * absente est le verdict rendu au client, compté à part — le lister parmi les
190
+ * erreurs ferait chercher une panne là où il manque une identité.
191
+ *
192
+ * @param entry - le profil (seuls statut et erreur sont lus).
193
+ */
194
+ export declare function isFailedProfile(entry: Pick<ProfileEntry, "status" | "error">): boolean;
195
+ /**
196
+ * Le chemin d'une URL de profil. Le profileur enregistre l'URL ABSOLUE
197
+ * (`http://hôte:port/chemin`) : un filtre écrit sur le chemin ne mordrait
198
+ * jamais dessus. Sans `URL` à construire — appelée sur chaque profil lu.
199
+ */
200
+ export declare function urlPath(url: string): string;
201
+ /**
202
+ * Crée un accumulateur de profils.
203
+ *
204
+ * @param options - seuil N+1, prise en compte de l'outillage, aléa injecté.
205
+ * @returns l'accumulateur ; son `result()` peut être appelé à tout moment.
206
+ */
207
+ export declare function createProfileAggregator(options?: IProfileAggregatorOptions): IProfileAggregator;
208
+ /**
209
+ * Agrège une liste de profils en une fois.
210
+ *
211
+ * @param entries - les profils, dans n'importe quel ordre.
212
+ * @param options - mêmes réglages que {@link createProfileAggregator}.
213
+ * @returns le rapport.
214
+ */
215
+ export declare function aggregateProfiles(entries: Iterable<ProfileEntry>, options?: IProfileAggregatorOptions): IProfileReport;