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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +12 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -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;
@@ -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";
@@ -117,7 +124,7 @@ export interface ProfileSummary {
117
124
  interface ProfilableContext {
118
125
  requestId?: string | undefined;
119
126
  userIdentifier?: string | null | undefined;
120
- type?: string | undefined;
127
+ type?: ProtocolName | undefined;
121
128
  scheme?: string | undefined;
122
129
  method?: string | null | undefined;
123
130
  url?: string | undefined;
@@ -134,15 +141,7 @@ interface ProfilableContext {
134
141
  startMs: number;
135
142
  durationMs?: number | undefined;
136
143
  }> | undefined;
137
- resolver?: {
138
- route?: {
139
- name?: string | undefined;
140
- } | null;
141
- controller?: {
142
- name?: string | undefined;
143
- } | null | undefined;
144
- actionName?: string | undefined;
145
- } | null | undefined;
144
+ resolver?: ProfiledResolver | null | undefined;
146
145
  /**
147
146
  * Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
148
147
  * que la payload ALS — les adapters ORM y poussent via `RequestContext`.
@@ -168,10 +167,50 @@ interface ProfilableContext {
168
167
  roles: string[] | null;
169
168
  } | null | undefined;
170
169
  }
171
- export declare class Profiler {
172
- private readonly _buf;
173
- private readonly _cap;
174
- 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);
175
214
  /**
176
215
  * Snapshot un `Context` en fin de requête. Tolérant aux champs absents
177
216
  * (handshake WS, erreur précoce). À appeler AVANT `context.clean()`.
@@ -185,9 +224,23 @@ export declare class Profiler {
185
224
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
186
225
  */
187
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;
188
241
  /** Nombre de profils en mémoire. */
189
242
  get size(): number;
190
- /** Vide le ring buffer. */
243
+ /** Vide les anneaux. */
191
244
  clear(): void;
192
245
  }
193
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;
@@ -140,8 +140,20 @@ export declare function forwardRequest(req: ProxiedRequest, res: ProxiedResponse
140
140
  * @returns la tête complète, CRLF final compris
141
141
  */
142
142
  export declare function rawHead(status: string, rawHeaders: readonly string[], skip?: ReadonlySet<string> | null, extra?: readonly string[]): string;
143
- /** Réponse brute minimale sur un socket d'upgrade, puis fermeture. */
144
- export declare function refuseUpgrade(socket: Duplex, status: number, reason: string): void;
143
+ /**
144
+ * Réponse brute minimale sur un socket d'upgrade, puis fermeture.
145
+ *
146
+ * Les en-têtes passent par la validation de `node:http` : un nom ou une valeur
147
+ * invalide — un saut de ligne surtout, qui forgerait un en-tête de plus — est
148
+ * OMIS, jamais écrit. `Connection` et `Content-Length` sont posés ici et ne se
149
+ * remplacent pas.
150
+ *
151
+ * @param socket - socket du client, encore en attente de sa réponse
152
+ * @param status - statut HTTP du refus
153
+ * @param reason - phrase de raison
154
+ * @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`…)
155
+ */
156
+ export declare function refuseUpgrade(socket: Duplex, status: number, reason: string, headers?: Readonly<Record<string, string | number>> | null): void;
145
157
  /**
146
158
  * Relaie un upgrade WebSocket DÉJÀ validé (méthode, `Upgrade`, clé, version,
147
159
  * `Origin` — cf `ReverseProxy.handleUpgrade`) : rejoue le handshake vers
@@ -42,6 +42,12 @@ export interface ProxyIntrospection {
42
42
  httpPort: number;
43
43
  /** Port HTTPS/2 du backend (re-encrypt). */
44
44
  httpsPort: number;
45
+ /**
46
+ * Port h2c du backend (`servers.h2c`), ou `null` s'il n'est pas activé. Le
47
+ * générateur haproxy y envoie le trafic en HTTP/2 (`proto h2`) ; nginx, qui
48
+ * ne parle pas HTTP/2 à ses amonts, l'ignore.
49
+ */
50
+ h2cPort?: number | null;
45
51
  /** Dossiers statiques servis à la racine `/` (ordre = priorité). */
46
52
  staticRoots: string[];
47
53
  /** Montages statiques préfixés (servis tels quels). */
@@ -71,6 +77,16 @@ export interface ProxyIntrospection {
71
77
  * saines.
72
78
  */
73
79
  keepaliveIntervalMs: number;
80
+ /**
81
+ * Chemin de la sonde de disponibilité (`health.readinessPath`), ou `null`
82
+ * si les sondes sont désactivées.
83
+ *
84
+ * HAProxy la sonde (`option httpchk`) au lieu d'un simple contrôle de
85
+ * connexion : un exemplaire qui draine garde son port ouvert, mais `/readyz`
86
+ * répond déjà `503` — c'est ce qui le retire du répartiteur AVANT qu'il
87
+ * cesse d'accepter. Un contrôle TCP le garderait jusqu'à la fermeture du port.
88
+ */
89
+ readinessPath?: string | null;
74
90
  /**
75
91
  * Terminaison TLS au frontal, ou `null` pour n'écouter qu'en clair.
76
92
  *
@@ -78,6 +78,9 @@ export declare function isAmbiguousPath(url: string): boolean;
78
78
  * un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
79
79
  * octet pour octet ouvrirait un tunnel que rien n'inspecte.
80
80
  *
81
+ * Appelé à CHAQUE handshake (proxy inverse et routes de Nodefony) : aucune
82
+ * allocation — des expressions testées, jamais de découpe de chaîne.
83
+ *
81
84
  * @param method - méthode de la requête
82
85
  * @param headers - en-têtes reçus
83
86
  * @returns la raison du refus, ou `null` si le handshake est conforme
@@ -0,0 +1,37 @@
1
+ import type { Http2SecureServer, Http2Server, ServerOptions } from "node:http2";
2
+ import type { IHttpConfig } from "../../config/config.js";
3
+ /**
4
+ * Limites anti-DoS d'un serveur HTTP/2, lues dans la section `http2` du module :
5
+ * `maxSessionMemory` (amplification mémoire) et `maxConcurrentStreams` dans les
6
+ * SETTINGS (CVE-2023-44487, Rapid Reset). Règle UNIQUE des serveurs HTTP/2 — TLS
7
+ * (5152) comme h2c — : une limite oubliée sur l'un d'eux en ferait la porte de
8
+ * l'attaque.
9
+ *
10
+ * Une limite absente garde le défaut de Node (aucune régression).
11
+ *
12
+ * @param limits - `module.options.http2`.
13
+ * @returns les options à fusionner dans celles de `createServer`/`createSecureServer`.
14
+ */
15
+ export declare function http2ServerLimits(limits: Partial<IHttpConfig["http2"]> | undefined): Pick<ServerOptions, "maxSessionMemory" | "settings">;
16
+ /**
17
+ * Borne l'inactivité des sessions d'un serveur HTTP/2 — TLS (5152) comme h2c —
18
+ * et contourne un défaut de Node qui garde chaque session fermée en vie jusqu'à
19
+ * l'échéance de ce délai.
20
+ *
21
+ * Le défaut (Node 24, 26) : `closeSession` (`internal/http2/core`) marque la
22
+ * session détruite PUIS appelle `session.setTimeout(0)` ; or `setStreamTimeout`
23
+ * sort aussitôt sur un objet détruit. Le minuteur n'est jamais désarmé et retient
24
+ * la session (~2,4 Ko) pendant `timeoutMs` : sous un flux de connexions neuves,
25
+ * connexions/s × délai × 2,4 Ko. Signalé à Node : https://github.com/nodejs/node/issues/66885
26
+ *
27
+ * Le contournement : désarmer à la réception du GOAWAY, AVANT la destruction —
28
+ * c'est ainsi que ferment un proxy et un client Node, même par `destroy()`. Rien
29
+ * n'est retiré à la protection : Node n'agit déjà plus à l'échéance sur une
30
+ * session fermée (`sessionOnTimeout` sort sur `closed`). Une coupure TCP sans
31
+ * GOAWAY garde la rétention, bornée au délai.
32
+ *
33
+ * @param server - le serveur HTTP/2.
34
+ * @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
35
+ * @param onTimeout - appelé à l'échéance (événement `timeout` du serveur).
36
+ */
37
+ export declare function setHttp2SessionTimeout(server: Http2Server | Http2SecureServer, timeoutMs: number | undefined, onTimeout: () => void): void;
@@ -10,6 +10,15 @@ import type { ServersConfig } from "nodefony";
10
10
  * @returns la section typée, ou `undefined` si elle est absente ou n'est pas un objet.
11
11
  */
12
12
  export declare function asServersConfig(servers: unknown): ServersConfig | undefined;
13
+ /**
14
+ * Le serveur h2c démarre-t-il ? Seul un `enabled: true` EXPLICITE l'allume : la
15
+ * section existe toujours (défauts), un port seul ne vaut pas activation.
16
+ *
17
+ * @param entry - `servers.h2c`.
18
+ */
19
+ export declare function isH2cEnabled(entry: {
20
+ enabled?: boolean | undefined;
21
+ } | undefined): boolean;
13
22
  /**
14
23
  * Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
15
24
  * serveur est désactivé, absent, ou sans port numérique.
@@ -17,4 +26,4 @@ export declare function asServersConfig(servers: unknown): ServersConfig | undef
17
26
  * @param servers - section `servers` déjà rétrécie ({@link asServersConfig}).
18
27
  * @param type - le serveur visé.
19
28
  */
20
- export declare function configuredServerPort(servers: ServersConfig | undefined, type: "http" | "https"): number | undefined;
29
+ export declare function configuredServerPort(servers: ServersConfig | undefined, type: "http" | "https" | "h2c"): number | undefined;