@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
@@ -13,6 +13,18 @@ export interface IWsHeartbeatOptions {
13
13
  /** Délai (ms) accordé pour recevoir le `pong` avant de couper la socket. */
14
14
  keepaliveGracePeriod?: number;
15
15
  }
16
+ /**
17
+ * Ce que le tick lit et écrit d'une connexion — la socket `ws` en production.
18
+ */
19
+ export interface IHeartbeatClient {
20
+ readonly readyState: number;
21
+ ping(): void;
22
+ terminate(): void;
23
+ /** Dernier signe de vie (date du dernier `pong`, ou de la connexion). */
24
+ _nfLastPong?: number;
25
+ /** Date du dernier `ping` émis ; `0` = aucun ping en attente de réponse. */
26
+ _nfPingedAt?: number;
27
+ }
16
28
  /**
17
29
  * Arme le suivi keep-alive d'UNE connexion : initialise l'horodatage de vie et
18
30
  * attache l'unique listener `pong` qui le rafraîchit.
@@ -24,6 +36,20 @@ export interface IWsHeartbeatOptions {
24
36
  * @param ws - la socket fraîchement connectée
25
37
  */
26
38
  export declare const trackPong: (ws: Ws) => void;
39
+ /**
40
+ * Un tick du keep-alive : pingue les connexions muettes depuis `interval`, et
41
+ * coupe celles dont le ping attend sa réponse depuis plus de `grace`.
42
+ *
43
+ * Fonction PURE du temps : l'horloge est passée en argument, si bien que la
44
+ * règle se prouve pas à pas, sans minuteur réel — un banc qui la prouverait à
45
+ * l'horloge murale confondrait une boucle d'événements chargée avec un zombie.
46
+ *
47
+ * @param clients - les connexions du serveur
48
+ * @param now - l'instant du tick (`performance.now()` en production)
49
+ * @param interval - délai sans signe de vie avant un ping, ms
50
+ * @param grace - délai accordé au pong avant de couper, ms
51
+ */
52
+ export declare const heartbeatTick: (clients: Iterable<IHeartbeatClient>, now: number, interval: number, grace: number) => void;
27
53
  /**
28
54
  * Démarre le heartbeat keep-alive du serveur : détecte les connexions zombies
29
55
  * (half-open — TCP encore ouvert mais le pair a disparu sans frame Close) et les
@@ -1,6 +1,6 @@
1
1
  import { Service, Module } from "nodefony";
2
2
  import type { IPage } from "nodefony";
3
- import type { ISessionStorage, ISessionSummary, ISessionRecord, ISessionListQuery } from "../../interfaces/ISession.js";
3
+ import type { ISessionStorage, ISerializedSession, ISessionSummary, ISessionRecord, ISessionListFilter, ISessionListQuery } from "../../interfaces/ISession.js";
4
4
  import HttpKernel, { ContextType } from "../http-kernel.js";
5
5
  import Session, { OptionsSessionType } from "../../src/session/session.js";
6
6
  import type { IHttpConfig } from "../../config/config.js";
@@ -191,6 +191,24 @@ declare class SessionsService extends Service {
191
191
  * est sûre. Idempotent — `false` si aucun `ref` ne correspond.
192
192
  */
193
193
  destroyByRef(ref: string, actor?: string | null): Promise<boolean>;
194
+ /**
195
+ * Détruit TOUTES les sessions dont le contenu satisfait `match` — la primitive
196
+ * des révocations groupées (« déconnecter partout », déconnexion demandée par
197
+ * un fournisseur d'identité). Ni journal ni audit : l'appelant, qui connaît le
198
+ * motif, les émet.
199
+ *
200
+ * @remarks Quand la méthode rend la main, il ne reste AUCUNE session qui
201
+ * satisfait `match` (au plus `MAX_LOGOUT_PASSES` passages). Une révocation qui
202
+ * en laisse une seule n'est pas une imprécision, c'est une faille.
203
+ *
204
+ * @param filter - restriction poussée au store (`user` : WHERE indexable) ;
205
+ * `undefined` parcourt tout le parc.
206
+ * @param match - prédicat sur la session sérialisée — il doit RE-VÉRIFIER ce
207
+ * que `filter` exprime, un store pouvant l'ignorer.
208
+ * @returns le nombre de sessions détruites.
209
+ * @throws Error si le store actif ne sait pas énumérer ses sessions.
210
+ */
211
+ destroyWhere(filter: ISessionListFilter | undefined, match: (data: ISerializedSession) => boolean): Promise<number>;
194
212
  /**
195
213
  * « Déconnexion partout » : détruit TOUTES les sessions d'un utilisateur (scan
196
214
  * O(N) — pas d'index inverse, acceptable en admin). Renvoie le nombre détruit.
@@ -1,5 +1,5 @@
1
1
  import { Container, Service, Severity, Msgid, Message, Pci, Pdu, KernelEventsType, nodefonyError, Scope } from "nodefony";
2
- import type { IProfilerQuery } from "nodefony";
2
+ import type { IProfilerQuery, RequestContextPayload } from "nodefony";
3
3
  import type { IRouteResolver, IRequestRouter } from "../../interfaces/IRouting.js";
4
4
  import type { ISecurityZone } from "../../interfaces/ISecurity.js";
5
5
  import { WebSocketServer } from "ws";
@@ -7,6 +7,7 @@ import http2 from "node:http2";
7
7
  import http from "node:http";
8
8
  import https from "node:https";
9
9
  import HttpKernel, { ServerType, Data, SchemeType } from "../../service/http-kernel.js";
10
+ import type { AlpnProtocol, ProtocolName } from "../../interfaces/IContext.js";
10
11
  import HttpResponse from "./http/Response.js";
11
12
  import Http2Response from "./http2/Response.js";
12
13
  import WebsocketResponse from "./websocket/Response.js";
@@ -43,7 +44,6 @@ declare class Context extends Service implements IContextInterface {
43
44
  secure: boolean;
44
45
  security?: ISecurityZone | null | undefined;
45
46
  cleaned: boolean;
46
- isControlledAccess: boolean;
47
47
  validDomain: boolean;
48
48
  finished: boolean;
49
49
  pushAllowed: boolean;
@@ -51,7 +51,15 @@ declare class Context extends Service implements IContextInterface {
51
51
  requested: boolean;
52
52
  sessionStarting: boolean;
53
53
  domain: string;
54
- type: ServerType;
54
+ /** Famille traitée (`network.protocol.name` d'OpenTelemetry) — union ouverte. */
55
+ type: ProtocolName;
56
+ /** Serveur qui a reçu la requête — choisit la configuration par serveur. */
57
+ serverType: ServerType;
58
+ /**
59
+ * Protocole du saut local (registre IANA ALPN) — union ouverte. Une connexion
60
+ * WebSocket naît d'un upgrade HTTP/1.1 ; `HttpContext` le constate sur la requête.
61
+ */
62
+ alpnProtocol: AlpnProtocol;
55
63
  server?: WebSocketServer | http.Server | https.Server | http2.Http2SecureServer | null | undefined;
56
64
  httpKernel: HttpKernel | null;
57
65
  request: contextRequest | null;
@@ -68,6 +76,14 @@ declare class Context extends Service implements IContextInterface {
68
76
  session: Session | null | undefined;
69
77
  cookieSession: Cookie | null | undefined;
70
78
  user: unknown;
79
+ /**
80
+ * Identifiant de l'appelant AUTHENTIFIÉ, posé par le pare-feu ; `null` hors
81
+ * zone ou pour un anonyme. Lisible au démontage, hors de la bulle ALS : c'est
82
+ * ce que lisent le journal d'audit et le profileur. Distinct de `user`, qui
83
+ * lie la session à son principal de SESSION — y écrire l'identité d'un jeton
84
+ * rattacherait la session d'un autre compte.
85
+ */
86
+ userIdentifier: string | null;
71
87
  waitAsync: boolean;
72
88
  isJson: boolean;
73
89
  protected _isHtml: boolean | null;
@@ -116,11 +132,12 @@ declare class Context extends Service implements IContextInterface {
116
132
  private _phaseIndex;
117
133
  private _afterResponseFns;
118
134
  private _afterResponseFired;
135
+ private requestStore;
119
136
  private _abortController;
120
137
  metaData: Data;
121
138
  scheme: SchemeType;
122
139
  webSocketState: WebSocketState;
123
- constructor(container: Container | Scope, type: ServerType);
140
+ constructor(container: Container | Scope, serverType: ServerType);
124
141
  /**
125
142
  * Assemble l'enveloppe `metaData` de la requête (`nodefony.*` + `route`
126
143
  * snapshot per-requête + overrides appelant). Délègue au builder monomorphe
@@ -160,11 +177,23 @@ declare class Context extends Service implements IContextInterface {
160
177
  get signal(): AbortSignal;
161
178
  _abortIfPending(reason?: string): void;
162
179
  log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
180
+ /**
181
+ * Confie au contexte le magasin `RequestContext` de son unité de travail
182
+ * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
183
+ *
184
+ * Un minuteur ou une promesse armé pendant l'unité de travail capture ce
185
+ * magasin et le garde au-delà de la réponse — avec le contexte, l'identité
186
+ * et le jeton du pare-feu. Le vidage ({@link RequestContext.release}) n'y
187
+ * laisse que la corrélation et le scope refermé.
188
+ *
189
+ * @param store - le magasin passé à `RequestContext.run`
190
+ */
191
+ holdRequestStore(store: RequestContextPayload): void;
163
192
  clean(): void;
164
193
  /**
165
194
  * Log d'un event du cycle de vie — gate boot-time (V2.1) : en production le
166
- * Pdu n'est PAS construit (return avant toute allocation). Hors prod, jalon
167
- * notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
195
+ * Pdu n'est PAS construit (return avant toute allocation). Hors prod : DEBUG,
196
+ * pour le Suivi de requête de Studio — la ligne `req` porte le bilan.
168
197
  */
169
198
  private logEvent;
170
199
  fire(event: KernelEventsType, ...args: unknown[]): boolean;
@@ -13,12 +13,22 @@
13
13
  * Politique de compilation (sûre, ancrée, ReDoS-free) :
14
14
  * - string sans `*` → match EXACT ancré (`^...$`, le `.` est littéral).
15
15
  * - string avec `*` → wildcard UN label (`*.example.com` → `^[^.]+\.example\.com$`,
16
- * RFC 6125 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
16
+ * RFC 9525 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
17
17
  * - `RegExp` → reprise telle quelle (l'auteur assume l'ancrage).
18
18
  *
19
19
  * Compilation faite UNE fois (boot / enregistrement de route) ; le test par
20
20
  * requête est un simple `RegExp.test` sur une liste pré-compilée (zéro alloc hot-path).
21
21
  */
22
+ /**
23
+ * Nom d'hôte d'une autorité brute (`host[:port]`), sans port — la valeur que
24
+ * rendrait `URL.hostname` pour une autorité déjà canonique, sans en payer le
25
+ * parse (`indexOf` + `slice`, #508). Un littéral IPv6 garde ses crochets
26
+ * (`[::1]:5151` → `[::1]`) : couper au premier `:` le réduirait à `[`.
27
+ *
28
+ * @param host - autorité brute (`Host`), éventuellement absente
29
+ * @returns le nom d'hôte, `""` si l'autorité est absente
30
+ */
31
+ export declare function hostnameOf(host: string | undefined): string;
22
32
  /** Un pattern de domaine : string (exact ou `*`-wildcard) ou `RegExp` (libre). */
23
33
  export type DomainPattern = string | RegExp;
24
34
  /**
@@ -30,17 +30,30 @@ export interface ProxyType {
30
30
  proxyRealIp?: string | undefined;
31
31
  proxyVia?: string | undefined;
32
32
  }
33
+ /**
34
+ * Même URL (chemin, requête, hôte) sous un autre schéma et un autre port.
35
+ * `port` absent → port par défaut du schéma (omis de l'URL).
36
+ *
37
+ * @param from - URL de la requête.
38
+ * @param scheme - schéma cible.
39
+ * @param port - port du serveur cible, s'il est connu.
40
+ * @returns l'URL absolue de redirection.
41
+ */
42
+ export declare function switchUrlScheme(from: URL, scheme: "http" | "https", port?: number): string;
33
43
  export type HttpRequestType = Http2Request | HttpRequest;
34
44
  export type HttpRsponseType = Http2Response | HttpResponse;
35
45
  import type { IHttpContext as IHttpContextInterface } from "../../../interfaces/IContext.js";
46
+ import { type ISseStreamOptions, type SseStream } from "./SseStream.js";
36
47
  declare class HttpContext extends Context implements IHttpContextInterface {
37
48
  proxy: ProxyType | null;
49
+ /** Socket h1 inscrite dans `socketActiveContext` — retirée par `clean()`. */
50
+ private activeSocket;
38
51
  isRedirect: boolean;
39
52
  sended: boolean;
40
53
  request: HttpRequestType;
41
54
  response: HttpRsponseType;
42
55
  uploadService: uploadService | null;
43
- constructor(container: Container | Scope, request: http.IncomingMessage | http2.Http2ServerRequest, response: http.ServerResponse | http2.Http2ServerResponse, type: ServerType);
56
+ constructor(container: Container | Scope, request: http.IncomingMessage | http2.Http2ServerRequest, response: http.ServerResponse | http2.Http2ServerResponse, serverType: ServerType);
44
57
  /**
45
58
  * URL d'origine effective, construite À LA DEMANDE (1ʳᵉ lecture, puis
46
59
  * mémoïsée). `Origin` invalide ou `null` (RFC 6454) → repli sur l'URL de la
@@ -71,6 +84,16 @@ declare class HttpContext extends Context implements IHttpContextInterface {
71
84
  */
72
85
  handle(): MaybePromise<this>;
73
86
  setTimeout(): void;
87
+ /**
88
+ * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
89
+ * persistante survit à la requête, et l'entrée de `socketActiveContext`
90
+ * retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
91
+ * pointe encore sur CE contexte est retirée — une requête suivante sur la
92
+ * même socket a pu la réécrire. Le gestionnaire `timeout` de la socket ne
93
+ * trouve alors rien et prend le chemin « socket inactive → `destroy()` »,
94
+ * celui qu'il prenait déjà pour un contexte nettoyé.
95
+ */
96
+ clean(): void;
74
97
  /**
75
98
  * Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
76
99
  * en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
@@ -115,6 +138,16 @@ declare class HttpContext extends Context implements IHttpContextInterface {
115
138
  * @returns la réponse, ou sa promesse quand l'écriture attend.
116
139
  */
117
140
  write(chunk: unknown, encoding?: BufferEncoding, _flush?: boolean): MaybePromise<Http2Response | HttpResponse>;
141
+ /**
142
+ * Répond par un flux d'événements serveur (`text/event-stream`).
143
+ *
144
+ * La requête ne se termine qu'à `close()` du flux, ou au départ du client.
145
+ *
146
+ * @param options - battement de cœur, délai `retry:` initial.
147
+ * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
148
+ * @throws HttpError 500 quand la réponse est déjà partie.
149
+ */
150
+ openSse(options?: ISseStreamOptions): SseStream | Promise<SseStream>;
118
151
  flush(chunk: unknown, encoding: BufferEncoding): MaybePromise<HttpResponse>;
119
152
  /**
120
153
  * Termine la requête : hook `onClose`, puis fin de la réponse si l'envoi ne
@@ -126,7 +159,21 @@ declare class HttpContext extends Context implements IHttpContextInterface {
126
159
  /** Fin de la réponse, sauf si l'envoi l'a déjà terminée. */
127
160
  private endResponse;
128
161
  redirect(Url: string, status?: number | string, headers?: Record<string, string | number>): HttpResponse;
162
+ /**
163
+ * Redirige vers la même URL en HTTPS, sur le port du serveur HTTPS lié —
164
+ * ou le port par défaut derrière un proxy, dont le port public est inconnu.
165
+ *
166
+ * @param status - code de redirection (défaut de `redirect`).
167
+ * @param headers - en-têtes ajoutés à la réponse.
168
+ */
129
169
  redirectHttps(status?: number | string, headers?: Record<string, string | number>): HttpResponse;
170
+ /**
171
+ * Redirige vers la même URL en HTTP, sur le port du serveur HTTP lié — ou
172
+ * le port par défaut derrière un proxy.
173
+ *
174
+ * @param status - code de redirection (défaut de `redirect`).
175
+ * @param headers - en-têtes ajoutés à la réponse.
176
+ */
130
177
  redirectHttp(status?: number | string, headers?: Record<string, string | number>): HttpResponse;
131
178
  getHostName(): string;
132
179
  getRemoteAddress(): string | null;
@@ -5,6 +5,7 @@ import { URL } from "node:url";
5
5
  import { HTTPMethod, Cookies } from "../Context.js";
6
6
  import QS from "qs";
7
7
  import { ParserXml, ParserQs, Parser, acceptParser } from "./parser.js";
8
+ import type { RequestBody } from "../../../interfaces/IRequest.js";
8
9
  import { UploadedFile } from "../../../service/upload/upload-service.js";
9
10
  import type { IParsedUploadFile, IUploadOptions } from "../../../interfaces/IUpload.js";
10
11
  import { Pci, Pdu, Message, Severity, Msgid } from "nodefony";
@@ -75,22 +76,24 @@ declare class HttpRequest {
75
76
  get url(): URL;
76
77
  set url(u: URL);
77
78
  parser: ParserType | null;
78
- queryPost: Record<string, unknown>;
79
- queryGet: Record<string, unknown>;
80
- queryFile: UploadedFile[];
81
- query: Record<string, unknown>;
82
79
  /**
83
- * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
84
- * Fastify, NestJS), alias de {@link queryPost}.
85
- *
86
- * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
87
- * corps : `body` ne rend jamais un paramètre d'URL. Le data plane admin
88
- * expose déjà cette sémantique (`IAdminRequest.body`) ; ce getter la rend
89
- * disponible partout.
80
+ * Le CORPS de la requête, parsé (JSON, urlencoded, XML, champs multipart) —
81
+ * nom universel de l'écosystème (Express, Fastify, NestJS) et celui
82
+ * d'`IAdminRequest.body`. Jamais un paramètre d'URL : ceux-là sont dans
83
+ * {@link query}, et les deux ne fusionnent pas.
84
+ */
85
+ body: RequestBody;
86
+ /**
87
+ * Les fichiers reçus dans un corps multipart, déjà écrits dans le répertoire
88
+ * temporaire — lus par `@UploadedFile()` / `@UploadedFiles()`. Les champs
89
+ * texte du même corps sont dans {@link body}.
90
90
  *
91
- * Getter (aucune allocation) : `queryPost` reste la source unique.
91
+ * Lecture seule : une requête sans upload rend un vide partagé et gelé,
92
+ * aucune allocation.
92
93
  */
93
- get body(): Record<string, unknown>;
94
+ get files(): readonly UploadedFile[];
95
+ /** Les paramètres d'URL, seuls — jamais un champ du corps ({@link body}). */
96
+ query: Record<string, unknown>;
94
97
  queryStringOptions: (QS.IParseOptions & {
95
98
  decoder?: undefined;
96
99
  }) | undefined;
@@ -112,7 +115,7 @@ declare class HttpRequest {
112
115
  * remplace le `once("onRequestEnd")` du ctor qui allouait 1 onceWrapper +
113
116
  * 1 closure à CHAQUE requête.
114
117
  *
115
- * Il porte le CORPS (`queryPost`), pas la fusion `query` : un seul sens de
118
+ * Il porte le CORPS (`body`), jamais les paramètres d'URL : un seul sens de
116
119
  * `body` dans tout le framework, celui de l'écosystème et d'`IAdminRequest`.
117
120
  */
118
121
  private fireRequestEnd;
@@ -228,7 +231,9 @@ declare class HttpRequest {
228
231
  /**
229
232
  * Scheme effectif côté client : `Forwarded`/`X-Forwarded-*` résolu de façon
230
233
  * canonique (this.forwarded, gated proxy de confiance) ; sinon le transport
231
- * réel. HTTP/2 remplace la lecture du socket par le pseudo-header `:scheme`.
234
+ * réel. Vaut pour HTTP/2 aussi : le pseudo-en-tête `:scheme` vient du client
235
+ * (RFC 9113 §8.3.1) et n'est jamais cru — sur h2c, `:scheme: https` ferait
236
+ * passer une connexion claire pour chiffrée.
232
237
  */
233
238
  protected resolveScheme(request: http.IncomingMessage | http2.Http2ServerRequest): string;
234
239
  getFullUrl(request?: http.IncomingMessage | http2.Http2ServerRequest): string;
@@ -22,6 +22,9 @@ declare class HttpResponse {
22
22
  * n'a été choisi (ni négociation, ni `render`, ni controller), émet le défaut
23
23
  * `this.contentType` (application/octet-stream) — comportement identique à
24
24
  * l'ancienne pose au constructeur, sans le ping-pong set/remove/re-set.
25
+ *
26
+ * Une réponse SANS corps n'annonce aucun type : `Content-Type` décrit le
27
+ * contenu (RFC 9110 §8.3), et il n'y en a pas.
25
28
  */
26
29
  protected ensureContentTypeHeader(): void;
27
30
  /**
@@ -47,6 +50,17 @@ declare class HttpResponse {
47
50
  deleteCookieByName(name: string): boolean;
48
51
  setCookies(): void | http.ServerResponse<http.IncomingMessage>;
49
52
  setCookie(cookie: Cookie): void | http.ServerResponse<http.IncomingMessage>;
53
+ /**
54
+ * Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
55
+ * seulement. Le journal de développement se lit par le plan d'administration
56
+ * — un identifiant de session y entrerait chez quiconque le consulte.
57
+ * La ligne n'est construite que si DEBUG passe le gate du syslog pour ce
58
+ * `msgid` (debug ciblé compris) : rien n'est alloué en production.
59
+ *
60
+ * @param cookie - le cookie posé.
61
+ * @param serialized - sa forme `Set-Cookie`, déjà calculée pour l'en-tête.
62
+ */
63
+ private logCookie;
50
64
  setHeader(name: string, value: number | string | readonly string[]): void | http.ServerResponse<http.IncomingMessage>;
51
65
  setHeaders(obj: OutgoingHttpHeaders): OutgoingHttpHeaders;
52
66
  setContentType(type?: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
@@ -0,0 +1,155 @@
1
+ import type HttpContext from "./HttpContext.js";
2
+ /**
3
+ * Le flux d'événements serveur (SSE) d'une réponse HTTP — `text/event-stream`.
4
+ *
5
+ * SSE n'est pas une méthode : c'est un FORMAT de réponse, choisi à l'exécution
6
+ * par l'action. La route reste déclarée `GET` ou `POST`, et le même `POST` peut
7
+ * répondre en JSON ou en flux selon l'en-tête `Accept`.
8
+ *
9
+ * Ce que la classe porte, et qu'une action écrite à la main oubliait :
10
+ * - les en-têtes du flux (`Cache-Control: no-cache, no-transform`,
11
+ * `X-Accel-Buffering: no` pour qu'un nginx ne mette pas le flux en tampon) ;
12
+ * - HTTP/1.1 ET HTTP/2 : en HTTP/1.1 Node découpe le corps en morceaux
13
+ * (RFC 9112 §7.1) ; en HTTP/2 l'écriture passe par le FLUX, et aucun
14
+ * `Transfer-Encoding` n'est posé (interdit, RFC 9113 §8.2.2) ;
15
+ * - la fermeture lue sur la RÉPONSE, jamais sur la requête : en HTTP/2,
16
+ * `request.on("close")` part dès la fin du corps de la requête — c'est le
17
+ * défaut qui avait tué l'ancien flux de la console d'administration ;
18
+ * - le délai d'inactivité de la requête DÉSARMÉ (un flux n'a pas de fin
19
+ * attendue), remplacé par un battement de cœur sur un minuteur `unref` ;
20
+ * - la contre-pression : un client lent freine l'émetteur qui attend `send()` ;
21
+ * un client qui ne lit PLUS est coupé au bout de `stallTimeout` ;
22
+ * - les gardes des connexions longues, les mêmes que pour une socket
23
+ * WebSocket : plafond de connexions par IP (`wsMaxConnectionsPerIp`, un seul
24
+ * budget pour les deux transports) et fermeture du flux quand l'identité qui
25
+ * l'a ouvert est révoquée (`RevocationWatch`, cœur).
26
+ *
27
+ * Norme : WHATWG HTML §9.2 (copie hors ligne
28
+ * `nodefony-framework-dev/references/rfc/specs/whatwg-sse.md`).
29
+ */
30
+ /** Options d'ouverture d'un flux. */
31
+ export interface ISseStreamOptions {
32
+ /**
33
+ * Période du battement de cœur (commentaire `:`), en ms entre 1 et 2^31-1 —
34
+ * `false` pour l'éteindre. Défaut : 15 000, la valeur que la norme suggère
35
+ * contre les proxys qui coupent une connexion muette (§9.2.7).
36
+ */
37
+ heartbeat?: number | false | undefined;
38
+ /** Délai de reconnexion imposé au client (champ `retry:`), en ms. */
39
+ retry?: number | undefined;
40
+ /**
41
+ * Délai maximal pendant lequel une écriture peut rester bloquée — le client
42
+ * ne lit plus —, en ms entre 1 et 2^31-1 ; au-delà, la connexion est coupée.
43
+ * Remplace le délai d'inactivité de la requête, désarmé pendant un flux.
44
+ * `false` pour l'éteindre. Défaut : 30 000.
45
+ */
46
+ stallTimeout?: number | false | undefined;
47
+ /**
48
+ * Taille maximale d'un événement écrit, en octets (UTF-8). Au-delà, `send()`
49
+ * lève une `RangeError` au lieu d'écrire. Défaut : 1 Mio.
50
+ */
51
+ maxEventBytes?: number | undefined;
52
+ /**
53
+ * Durée de vie maximale du flux, en ms entre 1 et 2^31-1 : il est fermé
54
+ * ensuite, et un `EventSource` se reconnecte — en repassant par le pare-feu.
55
+ * `false` (défaut) : pas de borne.
56
+ */
57
+ maxDuration?: number | false | undefined;
58
+ }
59
+ /** Ce qu'un événement porte en plus de ses données. */
60
+ export interface ISseSendOptions {
61
+ /** Type de l'événement (champ `event:`) — absent : `message`. */
62
+ event?: string | undefined;
63
+ /** Identifiant (champ `id:`) que le client renverra en `Last-Event-ID`. */
64
+ id?: string | undefined;
65
+ }
66
+ /** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
67
+ export declare const SSE_HEARTBEAT_MS = 15000;
68
+ /** Délai de blocage d'écriture par défaut, en ms. */
69
+ export declare const SSE_STALL_TIMEOUT_MS = 30000;
70
+ /** Taille maximale d'un événement par défaut, en octets (1 Mio). */
71
+ export declare const SSE_MAX_EVENT_BYTES = 1048576;
72
+ /**
73
+ * Un flux `text/event-stream` ouvert sur une réponse.
74
+ *
75
+ * Il s'obtient par {@link openSseStream} (ou `this.renderSse()` dans un
76
+ * contrôleur), jamais par `new` : l'ouverture écrit les en-têtes, et un flux
77
+ * sans en-têtes ne se rattrape pas.
78
+ */
79
+ export declare class SseStream {
80
+ #private;
81
+ /** Le flux est-il fermé — par le serveur (`close()`) ou par le client parti ? */
82
+ get closed(): boolean;
83
+ /**
84
+ * Envoie un événement.
85
+ *
86
+ * @param data - texte envoyé tel quel, ou valeur sérialisée en JSON ; un
87
+ * texte de plusieurs lignes part en autant de champs `data:`.
88
+ * @param options - type (`event`) et identifiant (`id`) de l'événement.
89
+ * @returns rien quand l'écriture est acceptée ; une promesse réglée au
90
+ * `drain` quand le client lit moins vite qu'on écrit — l'attendre borne la
91
+ * mémoire du serveur. Sans effet une fois le flux fermé.
92
+ * @throws TypeError quand `event` ou `id` contient une fin de ligne.
93
+ * @throws RangeError quand l'événement dépasse `maxEventBytes`.
94
+ */
95
+ send(data: unknown, options?: ISseSendOptions): Promise<void> | undefined;
96
+ /**
97
+ * Envoie un commentaire — ignoré par le client, utile pour garder une
98
+ * connexion vivante ou marquer le flux à la lecture.
99
+ *
100
+ * @returns comme {@link send}.
101
+ */
102
+ comment(text?: string): Promise<void> | undefined;
103
+ /**
104
+ * Impose au client un délai de reconnexion (champ `retry:`).
105
+ *
106
+ * @param ms - délai en millisecondes, entier positif.
107
+ * @returns comme {@link send}.
108
+ * @throws RangeError quand `ms` n'est pas un entier entre 0 et 2^31-1.
109
+ */
110
+ retry(ms: number): Promise<void> | undefined;
111
+ /**
112
+ * Appelle `listener` à la fermeture du flux, quelle qu'en soit la cause.
113
+ *
114
+ * L'écouteur est lié au contexte asynchrone de la requête au moment de
115
+ * l'enregistrement : la fermeture par le client part de la SOCKET, hors de
116
+ * la bulle de la requête, et un `RequestContext.get()` y rendrait `undefined`.
117
+ * Un flux déjà fermé appelle l'écouteur au tour suivant.
118
+ */
119
+ onClose(listener: () => void): void;
120
+ /**
121
+ * Ferme le flux et termine la requête. Idempotent.
122
+ *
123
+ * @returns la fin de la requête (hooks `onClose` du contexte compris).
124
+ */
125
+ close(): Promise<void> | undefined;
126
+ }
127
+ /**
128
+ * Le client accepte-t-il un flux d'événements ? Lit l'en-tête `Accept`.
129
+ *
130
+ * Un même `POST` répond en JSON ou en flux selon ce que demande le client
131
+ * (porte MCP) : c'est cette fonction qui tranche, et elle seule. Une plage
132
+ * `q=0` est un REFUS explicite (RFC 9110 §12.4.2) ; `*\/*` ne suffit pas — un
133
+ * client qui ne nomme pas le flux ne sait pas le lire.
134
+ *
135
+ * @param accept - valeur de l'en-tête `Accept`, absente ou multiple.
136
+ * @returns `true` quand `text/event-stream` est nommé avec un poids non nul.
137
+ */
138
+ export declare function acceptsEventStream(accept: string | string[] | undefined): boolean;
139
+ /**
140
+ * Ouvre un flux d'événements sur la réponse d'un contexte HTTP.
141
+ *
142
+ * Sauve la session d'abord — ses cookies partent avec les en-têtes, qu'on ne
143
+ * pourra plus toucher —, puis écrit les en-têtes du flux et désarme le délai
144
+ * d'inactivité de la requête.
145
+ *
146
+ * @param context - le contexte de la requête, pas encore répondu.
147
+ * @param options - battement de cœur, délai `retry:` initial.
148
+ * @returns le flux ouvert, ou sa promesse quand une session est à sauver.
149
+ * @throws HttpError 500 quand la réponse est déjà partie.
150
+ * @throws HttpError 429 quand l'IP du client a déjà tous ses flux et sockets
151
+ * (`wsMaxConnectionsPerIp`) — refusé AVANT d'écrire quoi que ce soit.
152
+ * @throws RangeError quand un délai n'est ni `false` ni un entier entre 1 et
153
+ * 2^31-1, ou que `maxEventBytes` n'est pas un entier positif.
154
+ */
155
+ export declare function openSseStream(context: HttpContext, options?: ISseStreamOptions): SseStream | Promise<SseStream>;
@@ -22,6 +22,22 @@ declare class Parser {
22
22
  protected ended(): Promise<void>;
23
23
  parse(): Promise<this>;
24
24
  }
25
+ /**
26
+ * Corps d'un type que le framework ne décode pas : livré TEL QU'IL EST REÇU,
27
+ * jamais remplacé par un objet vide qui mentirait.
28
+ *
29
+ * - `text/*` → une chaîne, décodée au `charset` du `Content-Type` (UTF-8 par
30
+ * défaut) — comme une trame texte RFC 6455 ;
31
+ * - tout autre type, et un corps sans `Content-Type` (RFC 9110 §8.3 : supposé
32
+ * `application/octet-stream`) → les OCTETS : le `Buffer` déjà assemblé dans
33
+ * `request.data`, sans copie — donc un `Uint8Array`, le type d'octets que
34
+ * la socket livrera aussi (#618). Un même schéma passe par les deux portes.
35
+ *
36
+ * Un corps VIDE laisse `body` tel quel : une requête sans charge ne change pas.
37
+ */
38
+ declare class ParserRaw extends Parser {
39
+ parse(): Promise<this>;
40
+ }
25
41
  declare class ParserQs extends Parser {
26
42
  parserOptions: QS.IParseOptions;
27
43
  charset: BufferEncoding;
@@ -35,7 +51,7 @@ declare class ParserXml extends Parser {
35
51
  parse(): Promise<this>;
36
52
  }
37
53
  /**
38
- * Parse un corps `application/json` (ou `*+json`) en objet → `queryPost`
54
+ * Parse un corps `application/json` (ou `*+json`) en objet → `body`
39
55
  * (source lue par le décorateur `@Body`). Reprend le rôle de l'ancien plugin
40
56
  * `json` de formidable, supprimé avec le passage à busboy (multipart seul).
41
57
  * Lenient : un corps JSON malformé est ignoré (pas de throw — `parse()` est
@@ -62,4 +78,4 @@ type AcceptEntry = {
62
78
  [key: string]: RegExp | number | string | undefined;
63
79
  };
64
80
  declare const acceptParser: (acc?: string) => AcceptEntry[];
65
- export { Parser, ParserXml, ParserQs, ParserJson, acceptParser };
81
+ export { Parser, ParserRaw, ParserXml, ParserQs, ParserJson, acceptParser };
@@ -5,10 +5,10 @@ import { HTTPMethod } from "../Context.js";
5
5
  declare class Http2Request extends HttpRequest {
6
6
  request: http2.Http2ServerRequest;
7
7
  constructor(request: http2.Http2ServerRequest, context: HttpContext);
8
+ /** `:authority`, sinon `Host` — cf `requestAuthority` (RFC 9113 §8.3.1). */
8
9
  getHost(): string | undefined;
9
10
  getUserAgent(): string | undefined;
10
11
  getMethod(): HTTPMethod;
11
12
  getRawTarget(): string | undefined;
12
- protected resolveScheme(): string;
13
13
  }
14
14
  export default Http2Request;
@@ -3,6 +3,11 @@ import type { MaybePromise } from "nodefony";
3
3
  import http from "node:http";
4
4
  import HttpContext from "../http/HttpContext.js";
5
5
  import HttpResponse from "../http/Response.js";
6
+ /** Ce que la fusion lit d'une réponse Node : ses en-têtes posés, nom par nom. */
7
+ interface IHeaderSource {
8
+ getHeaderNames(): string[];
9
+ getHeader(name: string): http.OutgoingHttpHeader | undefined;
10
+ }
6
11
  /**
7
12
  * Compose les en-têtes d'une réponse HTTP/2 : ceux posés sur la réponse, puis
8
13
  * ceux passés à `writeHead`, qui gagnent.
@@ -13,12 +18,17 @@ import HttpResponse from "../http/Response.js";
13
18
  * (`ERR_HTTP2_HEADER_SINGLE_VALUE`), soit une 500 sur tout média servi en
14
19
  * HTTP/2. HTTP/2 n'a de toute façon que des noms en minuscules (RFC 9113 §8.2.1).
15
20
  *
21
+ * Les en-têtes posés se lisent nom par nom (`getHeaderNames`, déjà en
22
+ * minuscules) : `getHeaders()` les recopiait d'abord dans un objet sans
23
+ * prototype, avant cette fusion — 3,8 µs par réponse pour 12 en-têtes,
24
+ * contre 1,1 µs en une passe.
25
+ *
16
26
  * @param statusMessage - message de statut, recopié en `x-status-message`
17
- * @param current - en-têtes déjà posés sur la réponse
27
+ * @param current - la réponse Node qui porte les en-têtes déjà posés
18
28
  * @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
19
29
  * @returns les en-têtes à transmettre à `respond()`
20
30
  */
21
- export declare function mergeResponseHeaders(statusMessage: string, current: http.OutgoingHttpHeaders, extra?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): http.OutgoingHttpHeaders;
31
+ export declare function mergeResponseHeaders(statusMessage: string, current: IHeaderSource, extra?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): http.OutgoingHttpHeaders;
22
32
  declare class Http2Response extends HttpResponse {
23
33
  statusCode: number;
24
34
  stream: http2.ServerHttp2Stream | null;
@@ -27,9 +37,9 @@ declare class Http2Response extends HttpResponse {
27
37
  isHeaderSent(): boolean;
28
38
  writeHead(statusCode?: number, headers?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): void;
29
39
  /**
30
- * Écrit le corps sur le flux HTTP/2. Synchrone pour une redirection, un flux
31
- * fermé ou le repli HTTP/1 ; une promesse quand le corps est écrit sur le
32
- * flux, réglée par le rappel d'écriture (#505).
40
+ * Écrit le corps sur le flux HTTP/2 et, pour une réponse unique, le termine
41
+ * d'un seul `end(corps)`. Synchrone, sauf en streaming (`flush()`) : une
42
+ * promesse réglée par le rappel d'écriture, la contre-pression (#505).
33
43
  *
34
44
  * @returns la réponse, ou sa promesse.
35
45
  */
@@ -0,0 +1,20 @@
1
+ import http2 from "node:http2";
2
+ import type http from "node:http";
3
+ /**
4
+ * La réponse a-t-elle été TERMINÉE par le serveur ? Règle UNIQUE, pour HTTP/1.1
5
+ * comme pour HTTP/2 — l'écouteur `close` du noyau (client parti → 499) et le
6
+ * timeout par flux de `HttpContext` l'appellent tous deux.
7
+ *
8
+ * Sous HTTP/2, la réponse Nodefony écrit et termine le FLUX
9
+ * (`stream.respond`/`stream.end`), jamais la réponse de compatibilité : son
10
+ * `writableEnded` reste à `false` alors que le client a tout reçu. Mais le
11
+ * flux seul ne suffit pas non plus : un flux ANNULÉ par le pair (RST_STREAM
12
+ * avant la fin) a LUI AUSSI `writableEnded === true`, Node terminant le côté
13
+ * écriture en le détruisant. `aborted` sépare la fin posée par le serveur de la
14
+ * coupure du client — même sémantique que `writableEnded` en HTTP/1.1.
15
+ *
16
+ * @param response - la réponse Node de la requête
17
+ * @returns `true` si le serveur a terminé la réponse (ou son flux HTTP/2) ;
18
+ * `false` s'il ne l'a pas terminée, client parti compris
19
+ */
20
+ export declare function responseEnded(response: http.ServerResponse | http2.Http2ServerResponse): boolean;