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

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 (103) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -25
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +6 -3
  7. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  8. package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
  9. package/dist/nodefony/service/audit-logger.js +3 -17
  10. package/dist/nodefony/service/error-renderer.js +6 -4
  11. package/dist/nodefony/service/http-kernel.js +191 -40
  12. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  13. package/dist/nodefony/service/request-logger.js +1 -1
  14. package/dist/nodefony/service/reverse-proxy.js +7 -2
  15. package/dist/nodefony/service/servers/clientError.js +28 -4
  16. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  17. package/dist/nodefony/service/servers/server-http.js +5 -3
  18. package/dist/nodefony/service/servers/server-https.js +12 -5
  19. package/dist/nodefony/service/servers/server-static.js +1 -6
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  21. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  22. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  23. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  24. package/dist/nodefony/service/upload/upload-service.js +2 -2
  25. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  26. package/dist/nodefony/src/context/Context.js +56 -13
  27. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  28. package/dist/nodefony/src/context/http/HttpContext.js +75 -25
  29. package/dist/nodefony/src/context/http/Request.js +15 -6
  30. package/dist/nodefony/src/context/http/Response.js +354 -35
  31. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  32. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  34. package/dist/nodefony/src/context/http2/Request.js +1 -1
  35. package/dist/nodefony/src/context/http2/Response.js +28 -5
  36. package/dist/nodefony/src/context/requestTrace.js +104 -0
  37. package/dist/nodefony/src/context/timingMode.js +21 -0
  38. package/dist/nodefony/src/context/transport.js +20 -1
  39. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  40. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
  42. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  43. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  44. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  45. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  46. package/dist/nodefony/src/cookies/cookie.js +54 -13
  47. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  48. package/dist/nodefony/src/profiler/Profiler.js +25 -22
  49. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  50. package/dist/nodefony/src/proxy/forward.js +109 -9
  51. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  52. package/dist/nodefony/src/proxy/rules.js +29 -14
  53. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  54. package/dist/types/index.d.ts +8 -0
  55. package/dist/types/nodefony/config/config.d.ts +8 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  57. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  58. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  61. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  62. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  64. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  65. package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
  66. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  68. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  69. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  70. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  71. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
  72. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  73. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  74. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  75. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  76. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  79. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  80. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  81. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
  83. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  84. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  85. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  86. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  87. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  88. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  89. package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
  90. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  91. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  92. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  93. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  94. package/docs/byte-stream.md +371 -0
  95. package/docs/cookies.md +13 -3
  96. package/docs/diagnostics-channel.md +303 -0
  97. package/docs/index.md +14 -1
  98. package/docs/observabilite.md +94 -16
  99. package/docs/reverse-proxy.md +33 -19
  100. package/docs/servers.md +26 -15
  101. package/docs/session.md +3 -3
  102. package/docs/sse.md +27 -20
  103. package/package.json +16 -16
@@ -97,6 +97,12 @@ declare class Context extends Service implements IContextInterface {
97
97
  crossDomain: boolean;
98
98
  router: IRequestRouter | null;
99
99
  resolver: IRouteResolver | null;
100
+ /**
101
+ * Module qui signe les lignes de CETTE requête : la ligne d'accès et les
102
+ * lignes du contrôleur. `null` = le module de la route (sans route : le
103
+ * journal du noyau). Un contrôleur le change : `this.context.logModule = "billing"`.
104
+ */
105
+ logModule: string | null;
100
106
  /**
101
107
  * Intent de session de la route courante (posé par le Resolver depuis
102
108
  * `@UseSession` / paramètre `@Session`). Pilote le point d'activation unique
@@ -128,6 +134,9 @@ declare class Context extends Service implements IContextInterface {
128
134
  securityTrace: ISecurityTrace | null;
129
135
  private readonly _timingEnabled;
130
136
  private readonly _timingVerbose;
137
+ private readonly _clockEnabled;
138
+ private readonly _clockStartMs;
139
+ private _durationMs;
131
140
  readonly phases: PhaseTiming[];
132
141
  private _phaseIndex;
133
142
  private _afterResponseFns;
@@ -161,6 +170,21 @@ declare class Context extends Service implements IContextInterface {
161
170
  get timingEnabled(): boolean;
162
171
  phaseStart(name: PhaseName): void;
163
172
  phaseEnd(name: PhaseName): void;
173
+ /**
174
+ * Arrête l'horloge de la requête et rend sa durée, en millisecondes.
175
+ *
176
+ * La durée court de la construction du contexte au PREMIER appel : le
177
+ * démontage HTTP (`close` de la réponse, dernier octet remis — la mesure
178
+ * d'OpenTelemetry et du `$request_time` de nginx), la ligne d'accès pour un
179
+ * handshake WebSocket. Les appels suivants rendent la même valeur : journal
180
+ * d'accès, profiler et trace verbose lisent UNE durée, jamais trois.
181
+ *
182
+ * Active par défaut dans tous les environnements, production comprise :
183
+ * seul `timing.enabled: false` la coupe (cf `resolveTimingMode`).
184
+ *
185
+ * @returns la durée, ou `null` si `timing.enabled` vaut `false`
186
+ */
187
+ stopClock(): number | null;
164
188
  onAfterResponse(fn: AfterResponseHandler): void;
165
189
  /**
166
190
  * Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
@@ -176,7 +200,7 @@ declare class Context extends Service implements IContextInterface {
176
200
  private runAfterResponseHooks;
177
201
  get signal(): AbortSignal;
178
202
  _abortIfPending(reason?: string): void;
179
- log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
203
+ log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message, moduleName?: string): Pdu;
180
204
  /**
181
205
  * Confie au contexte le magasin `RequestContext` de son unité de travail
182
206
  * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
@@ -0,0 +1,109 @@
1
+ import http from "node:http";
2
+ import { Readable } from "node:stream";
3
+ import type HttpContext from "./HttpContext.js";
4
+ /**
5
+ * Réponse en FLUX D'OCTETS — la sortie d'un processus, le relais d'un autre
6
+ * service, une archive construite à la volée : toute source d'octets qui n'est
7
+ * pas un fichier du disque.
8
+ *
9
+ * Le cycle de vie (prise de la réponse, session, fermeture lue sur la réponse,
10
+ * plafond par IP, révocation, coupure d'un client qui ne lit plus) est celui de
11
+ * la brique commune au SSE (`StreamLifecycle`). Ce fichier porte ce qui est
12
+ * propre aux octets :
13
+ * - les en-têtes au PREMIER morceau : une erreur de la source avant lui passe
14
+ * par le chemin d'erreur normal (`HttpKernel.onError`), donc un vrai 5xx ;
15
+ * - l'arrêt de la source par `destroy()`, jamais `unpipe()` — qui la laisse en
16
+ * pause, et un producteur bloqué en écriture ne meurt plus ;
17
+ * - un chien de garde sur une source MUETTE (`idleTimeout`) : un seul minuteur
18
+ * périodique par flux, qui compare le compteur d'octets — aucun minuteur ni
19
+ * appel d'horloge par morceau (mesuré : l'horodatage par morceau coûtait
20
+ * 2 à 6 % de CPU sur des morceaux de 4 Kio) ;
21
+ * - la coupure (jamais une fin propre) quand la source échoue après le premier
22
+ * octet : le client doit savoir que le corps est tronqué.
23
+ */
24
+ /**
25
+ * Raison de la fin d'un flux d'octets.
26
+ *
27
+ * - `end` : la source est finie, ou le serveur a fermé le flux (durée de vie,
28
+ * révocation de l'identité) ;
29
+ * - `client` : le client est parti ;
30
+ * - `error` : la source a échoué ;
31
+ * - `timeout` : la source est restée muette plus de `idleTimeout` ;
32
+ * - `stall` : le client ne lisait plus depuis `stallTimeout`.
33
+ *
34
+ * L'union PEUT S'AGRANDIR : un `switch` exhaustif garde une branche `default`.
35
+ */
36
+ export type ByteStreamEndReason = "end" | "client" | "error" | "timeout" | "stall";
37
+ /** Ce que rend la fin d'un flux d'octets. */
38
+ export interface IByteStreamResult {
39
+ /** Pourquoi le flux s'est terminé. */
40
+ reason: ByteStreamEndReason;
41
+ /** Octets écrits sur la réponse. */
42
+ bytes: number;
43
+ }
44
+ /** Options d'une réponse en flux d'octets. */
45
+ export interface IByteStreamOptions {
46
+ /** Statut de la réponse, entre 200 et 599. Défaut : 200. */
47
+ status?: number | undefined;
48
+ /**
49
+ * En-têtes de la réponse, écrits au premier morceau. `Content-Length` est
50
+ * repris s'il est fourni ; sinon HTTP/1.1 envoie par morceaux. Défauts posés
51
+ * seulement s'ils manquent — ni ici, ni déjà sur la réponse :
52
+ * `Content-Type: application/octet-stream`, et pour un flux `live`
53
+ * `Cache-Control: no-cache, no-transform` et `X-Accel-Buffering: no`.
54
+ */
55
+ headers?: http.OutgoingHttpHeaders | undefined;
56
+ /**
57
+ * Flux VIVANT (défaut `true`) : la sortie d'un processus, un relais — un
58
+ * proxy inverse ne doit pas le mettre en tampon, ni un cache le garder, d'où
59
+ * `Cache-Control: no-cache, no-transform` et `X-Accel-Buffering: no` par
60
+ * défaut. `false` pour une source FINIE et rejouable (un fichier, une
61
+ * archive) : aucun de ces deux en-têtes n'est posé.
62
+ */
63
+ live?: boolean | undefined;
64
+ /**
65
+ * Délai maximal sans un octet de la source, en ms ; `0` le désarme. Au-delà :
66
+ * 504 si rien n'est encore parti, coupure sinon. Le silence est constaté
67
+ * par un tour de garde tous les quarts de délai : la coupure tombe entre
68
+ * `idleTimeout` et 1,25 × `idleTimeout`. Défaut : le délai de réponse
69
+ * du serveur (`responseTimeout`, 30 s). À garder sous le délai de lecture du
70
+ * proxy inverse (`proxy_read_timeout`, 60 s chez nginx).
71
+ */
72
+ idleTimeout?: number | undefined;
73
+ /**
74
+ * Délai maximal pendant lequel une écriture peut rester bloquée — le client
75
+ * ne lit plus —, en ms ; au-delà, la connexion est coupée. `false` pour
76
+ * l'éteindre. Défaut : 30 000, celui du SSE.
77
+ */
78
+ stallTimeout?: number | false | undefined;
79
+ /** Durée de vie maximale du flux, en ms. `false` (défaut) : pas de borne. */
80
+ maxDuration?: number | false | undefined;
81
+ /**
82
+ * Appelé une fois à la fin du flux, avec sa raison — AVANT la fin de la
83
+ * requête et la résolution de la promesse. Lié au contexte asynchrone de la
84
+ * requête.
85
+ */
86
+ onClose?: ((reason: ByteStreamEndReason) => void) | undefined;
87
+ }
88
+ /** Une source d'octets : flux Node, ou flux web (`fetch().body`). */
89
+ export type ByteStreamSource = Readable | ReadableStream<Uint8Array>;
90
+ /**
91
+ * Répond par un flux d'octets sur la réponse d'un contexte HTTP.
92
+ *
93
+ * Prend la réponse à l'appel (le noyau ne peut plus répondre par-dessus) ;
94
+ * les en-têtes partent au premier morceau de la source. La promesse se règle à
95
+ * la fin du flux, quelle qu'en soit la raison, et ne rejette JAMAIS.
96
+ *
97
+ * @param context - le contexte de la requête, pas encore répondu.
98
+ * @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
99
+ * @param options - statut, en-têtes, délais, écouteur de fin.
100
+ * @returns la raison de la fin et le nombre d'octets envoyés.
101
+ * @throws TypeError quand la source est en mode objet.
102
+ * @throws RangeError quand une option est hors bornes.
103
+ * @throws HttpError 500 quand la réponse est déjà partie ; 429 quand l'IP a
104
+ * déjà tous ses flux et sockets (`wsMaxConnectionsPerIp`).
105
+ *
106
+ * La source appartient au flux dès l'appel : quand l'appel lève, elle est
107
+ * DÉTRUITE (un flux web est annulé) avant que l'erreur ne remonte.
108
+ */
109
+ export declare function openByteStream(context: HttpContext, source: ByteStreamSource, options?: IByteStreamOptions): Promise<IByteStreamResult>;
@@ -1,7 +1,8 @@
1
1
  import { ServerType, SchemeType } from "../../../service/http-kernel.js";
2
+ import HttpError from "../../errors/httpError.js";
2
3
  import Context, { HTTPMethod } from "../Context.js";
3
- import { Container, Scope } from "nodefony";
4
- import type { MaybePromise } from "nodefony";
4
+ import { Container, Scope, nodefonyError } from "nodefony";
5
+ import type { DefaultEventsMap, EventsMap, MaybePromise } from "nodefony";
5
6
  import HttpRequest from "./Request.js";
6
7
  import HttpResponse from "./Response.js";
7
8
  import Http2Request from "../http2/Request.js";
@@ -42,14 +43,41 @@ export interface ProxyType {
42
43
  export declare function switchUrlScheme(from: URL, scheme: "http" | "https", port?: number): string;
43
44
  export type HttpRequestType = Http2Request | HttpRequest;
44
45
  export type HttpRsponseType = Http2Response | HttpResponse;
46
+ /**
47
+ * Route le délai d'inactivité d'une socket HTTP/1.1 vers la requête qu'elle
48
+ * porte : 408/504 si une requête est en cours, fermeture si la connexion
49
+ * persistante est inactive. Écouteur `timeout` du SERVEUR — un seul pour
50
+ * toutes ses connexions —, que Node appelle avec la socket en argument
51
+ * (`socketOnTimeout` → `server.emit("timeout", socket)`).
52
+ *
53
+ * Pourquoi au serveur et pas sur la socket : Node y pose déjà son propre
54
+ * écouteur `timeout`, et un second faisait créer à `EventEmitter` un tableau
55
+ * d'écouteurs par CONNEXION. Ce site d'allocation, partagé avec tout le
56
+ * processus et toujours survivant (une connexion vit longtemps), finissait
57
+ * par être basculé dans le vieux tas par V8 (pretenuring) — dans un démarrage
58
+ * sur trois environ, mesuré sous charge. Le graphe de chaque requête y
59
+ * restait alors accroché jusqu'à la collecte complète suivante : +45 % de CPU
60
+ * par requête, 14 à 19 collectes complètes au lieu de 3. Un écouteur par
61
+ * serveur supprime aussi une fermeture et une entrée de registre par connexion.
62
+ *
63
+ * @param socket - la socket dont le délai a expiré ; une session HTTP/2 (même
64
+ * événement sur un serveur `allowHTTP1`) est laissée à son propre délai
65
+ */
66
+ export declare function routeSocketTimeout(socket: unknown): void;
45
67
  import type { IHttpContext as IHttpContextInterface } from "../../../interfaces/IContext.js";
46
68
  import { type ISseStreamOptions, type SseStream } from "./SseStream.js";
69
+ import { type ByteStreamSource, type IByteStreamOptions, type IByteStreamResult } from "./ByteStream.js";
47
70
  declare class HttpContext extends Context implements IHttpContextInterface {
48
71
  proxy: ProxyType | null;
49
72
  /** Socket h1 inscrite dans `socketActiveContext` — retirée par `clean()`. */
50
73
  private activeSocket;
51
74
  isRedirect: boolean;
52
75
  sended: boolean;
76
+ /**
77
+ * Requête relayée par le proxy inverse : la politique de ligne d'accès de
78
+ * son montage. `null` = servie par l'application (cas de toute requête).
79
+ */
80
+ relayAccessLog: "all" | "errors" | null;
53
81
  request: HttpRequestType;
54
82
  response: HttpRsponseType;
55
83
  uploadService: uploadService | null;
@@ -84,6 +112,14 @@ declare class HttpContext extends Context implements IHttpContextInterface {
84
112
  */
85
113
  handle(): MaybePromise<this>;
86
114
  setTimeout(): void;
115
+ /**
116
+ * Ligne d'accès — pour une requête RELAYÉE, avec le statut que l'amont a
117
+ * réellement rendu (le relais écrit la réponse brute, pas celle du
118
+ * contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
119
+ *
120
+ * @param httpError - l'erreur de la requête, s'il y en a une
121
+ */
122
+ logRequest(httpError?: Error | HttpError | nodefonyError | null): import("nodefony").Pdu | undefined;
87
123
  /**
88
124
  * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
89
125
  * persistante survit à la requête, et l'entrée de `socketActiveContext`
@@ -143,11 +179,31 @@ declare class HttpContext extends Context implements IHttpContextInterface {
143
179
  *
144
180
  * La requête ne se termine qu'à `close()` du flux, ou au départ du client.
145
181
  *
182
+ * @typeParam Events - carte des événements du flux (cf `SseStream`).
146
183
  * @param options - battement de cœur, délai `retry:` initial.
147
184
  * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
148
185
  * @throws HttpError 500 quand la réponse est déjà partie.
149
186
  */
150
- openSse(options?: ISseStreamOptions): SseStream | Promise<SseStream>;
187
+ openSse<Events extends EventsMap = DefaultEventsMap>(options?: ISseStreamOptions): SseStream<Events> | Promise<SseStream<Events>>;
188
+ /**
189
+ * Répond par un FLUX D'OCTETS : un `<video src>`, un `fetch()`, un
190
+ * téléchargement ou un outil tiers (curl) le consomment comme n'importe quelle
191
+ * réponse HTTP, une réponse par consommateur.
192
+ *
193
+ * La réponse est prise à l'appel, ses en-têtes partent au premier morceau de
194
+ * la source. La source est DÉTRUITE à la fin, quelle qu'en soit la raison
195
+ * (jamais `unpipe`, qui laisse un producteur bloqué en écriture). Pour des
196
+ * événements texte lus par `EventSource`, voir {@link HttpContext.openSse}.
197
+ * Dans un middleware, la requête est répondue : ne pas appeler la suite.
198
+ *
199
+ * @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
200
+ * @param options - statut, en-têtes, délais, écouteur de fin.
201
+ * @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
202
+ * @throws HttpError 500 quand la réponse est déjà partie, 429 au-delà du
203
+ * plafond de connexions par IP ; TypeError pour une source en mode objet.
204
+ * Quand l'appel lève, la source est DÉTRUITE (un flux web est annulé).
205
+ */
206
+ openStream(source: ByteStreamSource, options?: IByteStreamOptions): Promise<IByteStreamResult>;
151
207
  flush(chunk: unknown, encoding: BufferEncoding): MaybePromise<HttpResponse>;
152
208
  /**
153
209
  * Termine la requête : hook `onClose`, puis fin de la réponse si l'envoi ne
@@ -180,7 +236,7 @@ declare class HttpContext extends Context implements IHttpContextInterface {
180
236
  getHost(): string | undefined;
181
237
  getUserAgent(): string | undefined;
182
238
  getMethod(): HTTPMethod;
183
- setContentType(type?: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
239
+ setContentType(type?: string, encoding?: BufferEncoding): void;
184
240
  protected resolveIsHtml(): boolean;
185
241
  setDefaultContentType(): void;
186
242
  setContextJson(encoding?: BufferEncoding): void;
@@ -220,12 +220,19 @@ declare class HttpRequest {
220
220
  getDomain(): string;
221
221
  getUserAgent(): string | undefined;
222
222
  getHostName(host?: string): string;
223
+ /**
224
+ * L'autorité de la requête : celle de la cible en forme absolue, sinon
225
+ * l'en-tête `Host` (RFC 9112 §3.2.2 — le serveur DOIT alors ignorer `Host`).
226
+ * HTTP/2 lit `:authority` (override).
227
+ */
223
228
  getHost(): string | undefined;
224
229
  getRemoteAddress(): string | null;
225
230
  /**
226
231
  * Request-target brut (origin-form) tel que reçu du transport — SOURCE
227
232
  * UNIQUE partagée par la concaténation de {@link getFullUrl} et la découpe
228
- * fast-path du ctor. HTTP/2 lit le pseudo-header `:path` (override).
233
+ * fast-path du ctor. Une cible en forme absolue est ramenée à sa forme
234
+ * origine, son autorité passant par {@link getHost}. HTTP/2 lit le
235
+ * pseudo-header `:path` (override).
229
236
  */
230
237
  getRawTarget(): string | undefined;
231
238
  /**
@@ -1,9 +1,21 @@
1
- import http, { OutgoingHttpHeaders } from "node:http";
1
+ import http, { OutgoingHttpHeaders, OutgoingHttpHeader } from "node:http";
2
2
  import http2 from "node:http2";
3
3
  import HttpContext from "../http/HttpContext.js";
4
4
  import { Pci, Pdu, Message, Severity, Msgid } from "nodefony";
5
5
  import type { MaybePromise } from "nodefony";
6
6
  import Cookie from "../../cookies/cookie.js";
7
+ import type { IFirewallGate } from "../../../interfaces/ISecurity.js";
8
+ /**
9
+ * Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
10
+ * 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
11
+ *
12
+ * Seule source de cette liste : la réponse s'en sert pour ne jamais écrire de
13
+ * corps, le résolveur pour terminer une action qui ne rend rien.
14
+ *
15
+ * @param status - le code de statut de la réponse.
16
+ * @returns `true` quand aucun corps ne doit partir.
17
+ */
18
+ export declare function isContentlessStatus(status: number): boolean;
7
19
  declare class HttpResponse {
8
20
  #private;
9
21
  context: HttpContext;
@@ -16,6 +28,13 @@ declare class HttpResponse {
16
28
  headers: http.OutgoingHttpHeaders;
17
29
  timeout?: number | undefined;
18
30
  cookies: Record<string, Cookie>;
31
+ /**
32
+ * Pare-feu qui posera les en-têtes de sécurité applicatifs juste avant
33
+ * l'émission des en-têtes, une fois le type de la réponse fixé : la politique
34
+ * suit ce type (ADR-0024 §C11). Armé par le noyau HTTP au routage, consommé
35
+ * une fois ; `null` sans module de sécurité.
36
+ */
37
+ headerPolicy: IFirewallGate | null;
19
38
  constructor(response: http.ServerResponse | http2.Http2ServerResponse, context: HttpContext);
20
39
  /**
21
40
  * Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
@@ -36,7 +55,9 @@ declare class HttpResponse {
36
55
  /**
37
56
  * Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
38
57
  * les octets — vide légal si rien n'a été posé (`write(null)` jetterait
39
- * ERR_STREAM_NULL_VALUES).
58
+ * ERR_STREAM_NULL_VALUES). Vide aussi sous un statut sans contenu : un corps
59
+ * posé par l'application sous un 205 ne part pas (RFC 9110 §15.3.6) — Node
60
+ * ne l'écarte de lui-même que pour 204 et 304.
40
61
  */
41
62
  protected get payload(): string | Buffer;
42
63
  /** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
@@ -48,8 +69,8 @@ declare class HttpResponse {
48
69
  addCookie(cookie: Cookie): Cookie;
49
70
  deleteCookie(cookie: Cookie): boolean;
50
71
  deleteCookieByName(name: string): boolean;
51
- setCookies(): void | http.ServerResponse<http.IncomingMessage>;
52
- setCookie(cookie: Cookie): void | http.ServerResponse<http.IncomingMessage>;
72
+ setCookies(): void;
73
+ setCookie(cookie: Cookie): void;
53
74
  /**
54
75
  * Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
55
76
  * seulement. Le journal de développement se lit par le plan d'administration
@@ -61,11 +82,91 @@ declare class HttpResponse {
61
82
  * @param serialized - sa forme `Set-Cookie`, déjà calculée pour l'en-tête.
62
83
  */
63
84
  private logCookie;
64
- setHeader(name: string, value: number | string | readonly string[]): void | http.ServerResponse<http.IncomingMessage>;
85
+ /**
86
+ * Pose un en-tête de la réponse ; en flux chunké déjà ouvert, le range en
87
+ * trailer. Le nom et la valeur sont VALIDÉS ici, comme le ferait Node : une
88
+ * valeur invalide (saut de ligne…) lève tout de suite et n'entre jamais dans
89
+ * la réserve — sinon elle lèverait au `writeHead`, et la page d'erreur
90
+ * rejouerait le même en-tête.
91
+ *
92
+ * @param name - nom de l'en-tête (casse indifférente).
93
+ * @param value - valeur ; une liste émet une ligne par élément (`Set-Cookie`).
94
+ * @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR` sur un nom
95
+ * ou une valeur qu'un en-tête HTTP ne peut pas porter.
96
+ */
97
+ setHeader(name: string, value: number | string | readonly string[]): void;
98
+ /**
99
+ * Pose un en-tête dont la valeur a DÉJÀ été validée par l'appelant — une
100
+ * table figée et contrôlée au démarrage (en-têtes de sécurité). Node la
101
+ * validera une fois, au `writeHead` ; la revalider ici effaçait le gain de
102
+ * l'écriture groupée (`micro-write-head.mjs`, ligne « plat validé »).
103
+ *
104
+ * ⚠️ Jamais pour une valeur venue d'une requête : elle passe par {@link setHeader}.
105
+ *
106
+ * @param name - nom de l'en-tête (casse indifférente).
107
+ * @param value - valeur contrôlée au démarrage.
108
+ */
109
+ setValidatedHeader(name: string, value: string): void;
110
+ /**
111
+ * Pose un en-tête validé au démarrage SAUF si la réponse en porte déjà un :
112
+ * la valeur du framework est un défaut, celle que l'application a écrite
113
+ * gagne. Même coût que {@link setValidatedHeader} — le rangement cherche
114
+ * l'en-tête de toute façon.
115
+ *
116
+ * @param name - nom de l'en-tête (casse indifférente).
117
+ * @param value - valeur contrôlée au démarrage.
118
+ */
119
+ setDefaultHeader(name: string, value: string): void;
120
+ /**
121
+ * Pose les en-têtes de la politique armée ({@link headerPolicy}) pour une
122
+ * réponse de type `contentType` — une fois, juste avant l'émission.
123
+ *
124
+ * Un `304` n'en reçoit aucun : ses en-têtes REMPLACENT ceux de la
125
+ * représentation en cache (RFC 9111 §4.3.4). Une politique courte y
126
+ * réduirait la page gardée, un nonce neuf la désaccorderait de ses balises.
127
+ *
128
+ * @param contentType - type de la réponse ; `null` = sans contenu ; `undefined`
129
+ * = inconnu (politique complète).
130
+ * @param servedAsFile - corps écrit par un tiers (fichier statique, relais),
131
+ * qui ne peut pas porter de nonce.
132
+ */
133
+ protected applyHeaderPolicy(contentType: string | null | undefined, servedAsFile?: boolean): void;
134
+ /**
135
+ * Type qui choisit la politique : `null` pour une réponse sans contenu (204,
136
+ * corps vide) — même si un `Content-Type` y a été posé, rien n'y sera rendu ;
137
+ * sinon le `Content-Type` posé.
138
+ */
139
+ protected contentTypeForPolicy(): string | null;
140
+ /**
141
+ * Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
142
+ * passer par {@link writeHead} — fichier statique, relais vers un amont. La
143
+ * réserve se vide ; ce qui se pose ensuite y retourne, et Node fusionne les
144
+ * deux au `writeHead`.
145
+ */
146
+ commitHeaders(): void;
147
+ /**
148
+ * Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
149
+ * y fera — fichier statique servi, redirection de dossier — et seulement
150
+ * alors : un raté du repli statique (la 404 d'une route inconnue) ne recopie
151
+ * rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
152
+ * {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
153
+ *
154
+ * L'interception est une propriété PROPRE de la réponse, ajoutée puis
155
+ * retirée : V8 annule alors la dernière transition de forme de l'objet.
156
+ */
157
+ commitBeforeNativeWrite(): void;
158
+ /** Retire l'interception posée par {@link commitBeforeNativeWrite}. */
159
+ disarmNativeWrite(): void;
160
+ /**
161
+ * Retire un en-tête, de la réserve comme de la réponse Node.
162
+ *
163
+ * @param name - nom de l'en-tête (casse indifférente).
164
+ */
165
+ removeHeader(name: string): void;
65
166
  setHeaders(obj: OutgoingHttpHeaders): OutgoingHttpHeaders;
66
- setContentType(type?: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
67
- setFileMimeType(type: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
68
- setContentTypeByExtension(extention: string): void | http.ServerResponse<http.IncomingMessage>;
167
+ setContentType(type?: string, encoding?: BufferEncoding): void;
168
+ setFileMimeType(type: string, encoding?: BufferEncoding): void;
169
+ setContentTypeByExtension(extention: string): void;
69
170
  getMimeType(filenameOrExt: string): string | false;
70
171
  setEncoding(encoding: BufferEncoding): BufferEncoding;
71
172
  setStatusCode(status: number | string, message?: string): {
@@ -88,7 +189,25 @@ declare class HttpResponse {
88
189
  setBody(ele: unknown, encoding?: BufferEncoding): void;
89
190
  setLength(body?: string | NodeJS.ArrayBufferView | ArrayBuffer | SharedArrayBuffer): number;
90
191
  writeHead(statusCode?: number, headers?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): void;
192
+ /** La réserve amorcée — pour une remise sous une autre forme (HTTP/2). */
193
+ protected headerList(): readonly OutgoingHttpHeader[];
91
194
  addTrailers(headers: http.OutgoingHttpHeaders): void;
195
+ /**
196
+ * Écrit un morceau d'une réponse en flux, sans la terminer — c'est `end()`
197
+ * qui la clôt après le dernier.
198
+ *
199
+ * Au premier morceau, les en-têtes partent SANS longueur ni corps posé (le
200
+ * patron de `SseStream`), et le cadrage revient à Node : `chunked` pour un
201
+ * client HTTP/1.1, fermeture de connexion pour un HTTP/1.0 (RFC 9112 §6.1 —
202
+ * pas de `Transfer-Encoding` à qui ne le connaît pas), trames DATA en HTTP/2
203
+ * où l'en-tête est interdit (RFC 9113 §8.2.2). Ne JAMAIS le poser à la main :
204
+ * une fois `flushing` levé, `setHeader` range en trailer, et un
205
+ * `Transfer-Encoding` en trailer est un message malformé (RFC 9110 §6.5.1).
206
+ *
207
+ * @param chunk - le morceau à écrire.
208
+ * @param encoding - l'encodage d'un morceau texte.
209
+ * @returns la promesse réglée quand le flux accepte l'écriture suivante.
210
+ */
92
211
  flush(chunk: unknown, encoding: BufferEncoding): MaybePromise<HttpResponse>;
93
212
  /**
94
213
  * Écrit le corps sur le flux et, pour une réponse unique, la termine.
@@ -113,8 +232,15 @@ declare class HttpResponse {
113
232
  * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
114
233
  */
115
234
  end(chunk?: string | Buffer, encoding?: BufferEncoding): MaybePromise<http.ServerResponse | http2.ServerHttp2Stream>;
235
+ /**
236
+ * Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
237
+ * qu'un tiers y a écrit directement : `send`, quota).
238
+ *
239
+ * @param name - nom de l'en-tête (casse indifférente).
240
+ */
116
241
  getHeader(name: string): string | number | string[] | undefined;
117
242
  hasHeader(name: string): boolean;
243
+ /** Tous les en-têtes posés, noms en minuscules — une copie, à chaque appel. */
118
244
  getHeaders(): http.OutgoingHttpHeaders;
119
245
  redirect(url: string, status?: number | string, headers?: Record<string, string | number>): this;
120
246
  log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
@@ -1,4 +1,5 @@
1
1
  import type HttpContext from "./HttpContext.js";
2
+ import type { DefaultEventsMap, EventNames, EventsMap } from "nodefony";
2
3
  /**
3
4
  * Le flux d'événements serveur (SSE) d'une réponse HTTP — `text/event-stream`.
4
5
  *
@@ -56,10 +57,14 @@ export interface ISseStreamOptions {
56
57
  */
57
58
  maxDuration?: number | false | undefined;
58
59
  }
59
- /** Ce qu'un événement porte en plus de ses données. */
60
- export interface ISseSendOptions {
60
+ /**
61
+ * Ce qu'un événement porte en plus de ses données.
62
+ *
63
+ * @typeParam K - nom de l'événement : un canal du contrat quand le flux en a un.
64
+ */
65
+ export interface ISseSendOptions<K extends string = string> {
61
66
  /** Type de l'événement (champ `event:`) — absent : `message`. */
62
- event?: string | undefined;
67
+ event?: K | undefined;
63
68
  /** Identifiant (champ `id:`) que le client renverra en `Last-Event-ID`. */
64
69
  id?: string | undefined;
65
70
  }
@@ -69,14 +74,32 @@ export declare const SSE_HEARTBEAT_MS = 15000;
69
74
  export declare const SSE_STALL_TIMEOUT_MS = 30000;
70
75
  /** Taille maximale d'un événement par défaut, en octets (1 Mio). */
71
76
  export declare const SSE_MAX_EVENT_BYTES = 1048576;
77
+ /**
78
+ * Charge d'un événement SSE : celle du canal nommé par `event`, ou — sans
79
+ * `event` (« un flux = un canal ») — l'une de celles de la carte. Carte non
80
+ * déclarée : `unknown`.
81
+ */
82
+ export type SseEventData<Events extends EventsMap, K extends string> = [
83
+ K
84
+ ] extends [never] ? Events[EventNames<Events>] : Events[K];
72
85
  /**
73
86
  * Un flux `text/event-stream` ouvert sur une réponse.
74
87
  *
75
88
  * Il s'obtient par {@link openSseStream} (ou `this.renderSse()` dans un
76
89
  * contrôleur), jamais par `new` : l'ouverture écrit les en-têtes, et un flux
77
90
  * sans en-têtes ne se rattrape pas.
91
+ *
92
+ * Typé par une carte d'événements (`ContractServerEmit<C>` : le MÊME contrat
93
+ * que la socket temps réel) : `event:` est un canal de la carte et la charge est
94
+ * celle du canal. Deux formes se lisent sur la même carte — **multiplexé**
95
+ * (`send(data, { event: canal })`, plusieurs canaux sur une connexion) ou **un
96
+ * flux = un canal** (carte d'un seul canal, `send(data)` sans `event`). Sans
97
+ * carte, le flux reste libre (porte MCP, flux hors contrat). Le type n'est pas
98
+ * une validation, et ne coûte rien à l'exécution.
99
+ *
100
+ * @typeParam Events - carte des événements du flux.
78
101
  */
79
- export declare class SseStream {
102
+ export declare class SseStream<Events extends EventsMap = DefaultEventsMap> {
80
103
  #private;
81
104
  /** Le flux est-il fermé — par le serveur (`close()`) ou par le client parti ? */
82
105
  get closed(): boolean;
@@ -89,10 +112,12 @@ export declare class SseStream {
89
112
  * @returns rien quand l'écriture est acceptée ; une promesse réglée au
90
113
  * `drain` quand le client lit moins vite qu'on écrit — l'attendre borne la
91
114
  * mémoire du serveur. Sans effet une fois le flux fermé.
92
- * @throws TypeError quand `event` ou `id` contient une fin de ligne.
115
+ * @throws TypeError quand `event` ou `id` contient une fin de ligne, ou
116
+ * quand `data` est fait d'octets (`Uint8Array`, `Buffer`, `ArrayBuffer`) :
117
+ * un flux SSE ne porte que du texte, les encoder soi-même (base64).
93
118
  * @throws RangeError quand l'événement dépasse `maxEventBytes`.
94
119
  */
95
- send(data: unknown, options?: ISseSendOptions): Promise<void> | undefined;
120
+ send<K extends EventNames<Events> = never>(data: SseEventData<Events, K>, options?: ISseSendOptions<K>): Promise<void> | undefined;
96
121
  /**
97
122
  * Envoie un commentaire — ignoré par le client, utile pour garder une
98
123
  * connexion vivante ou marquer le flux à la lecture.
@@ -152,4 +177,4 @@ export declare function acceptsEventStream(accept: string | string[] | undefined
152
177
  * @throws RangeError quand un délai n'est ni `false` ni un entier entre 1 et
153
178
  * 2^31-1, ou que `maxEventBytes` n'est pas un entier positif.
154
179
  */
155
- export declare function openSseStream(context: HttpContext, options?: ISseStreamOptions): SseStream | Promise<SseStream>;
180
+ export declare function openSseStream<Events extends EventsMap = DefaultEventsMap>(context: HttpContext, options?: ISseStreamOptions): SseStream<Events> | Promise<SseStream<Events>>;
@@ -0,0 +1,53 @@
1
+ import http from "node:http";
2
+ import type http2 from "node:http2";
3
+ /**
4
+ * Cycle de vie d'une réponse en flux — la brique commune au flux d'événements
5
+ * (`SseStream`) et au flux d'octets, NON exportée.
6
+ *
7
+ * Elle porte ce qu'un flux doit à la requête qui l'ouvre, quel que soit son
8
+ * format :
9
+ * - la PRISE de la réponse (`sended`), avant toute attente : le noyau ne peut
10
+ * plus répondre par-dessus, et l'avertissement « réponse non envoyée » se tait ;
11
+ * - la session sauvée AVANT les en-têtes — ses cookies partent avec eux ;
12
+ * - la fermeture lue sur la RÉPONSE, écouteur posé de façon SYNCHRONE à
13
+ * l'ouverture : un client parti pendant la sauvegarde de la session est vu ;
14
+ * - le délai d'inactivité de la requête désarmé, remplacé par les gardes du
15
+ * flux : coupure d'un client qui ne lit plus (`stallTimeout`), durée de vie
16
+ * maximale (`maxDuration`) ;
17
+ * - le plafond de connexions longues par IP (`wsMaxConnectionsPerIp`, le budget
18
+ * des sockets WebSocket) et la fermeture à la révocation de l'identité ;
19
+ * - une fin unique, et des écouteurs de fin liés au contexte asynchrone.
20
+ *
21
+ * Le FORMAT (en-têtes, écriture, battement de cœur) appartient au flux qui la
22
+ * porte.
23
+ */
24
+ /**
25
+ * Plus long délai qu'un minuteur sait tenir (2^31-1 ms). Au-delà, Node le
26
+ * ramène à 1 ms : un délai qui le dépasse ferait battre ou couper le flux
27
+ * mille fois par seconde.
28
+ */
29
+ export declare const MAX_TIMER_MS = 2147483647;
30
+ /**
31
+ * Qui a fermé le flux, quand ce n'est pas le client : le serveur (`close()` —
32
+ * fin demandée, durée de vie, révocation), ou la coupure d'un client qui ne lit
33
+ * plus. `null` : le client est parti, ou le flux est encore ouvert.
34
+ */
35
+ export type StreamLifecycleCause = "server" | "stall" | null;
36
+ /** Ce sur quoi on écrit : la réponse HTTP/1.1, ou le flux HTTP/2. */
37
+ export type StreamTarget = http.ServerResponse | http2.ServerHttp2Stream;
38
+ /** Gardes et identité d'un flux, options résolues. */
39
+ export interface IStreamLifecycleSettings {
40
+ stallTimeout: number | false;
41
+ maxDuration: number | false;
42
+ /** Revalidateur de l'identité du flux — `null` pour un anonyme. */
43
+ revalidate: (() => Promise<boolean>) | null;
44
+ /** Identifiant de connexion de l'identité qui ouvre le flux — `null` anonyme. */
45
+ identifier: string | null;
46
+ }
47
+ /** Ce que l'ouverture reçoit de l'appelant. */
48
+ export interface IOpenStreamLifecycle {
49
+ /** Préfixe des messages d'erreur (`SSE`, `Flux`). */
50
+ label: string;
51
+ stallTimeout: number | false;
52
+ maxDuration: number | false;
53
+ }