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

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 (84) hide show
  1. package/README.md +5 -3
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +11 -5
  6. package/dist/nodefony/command/certificatesCommand.js +0 -1
  7. package/dist/nodefony/config/config.js +48 -7
  8. package/dist/nodefony/config/defineModuleConfig.js +1 -1
  9. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  10. package/dist/nodefony/service/HttpAdminApi.js +0 -1
  11. package/dist/nodefony/service/audit-logger.js +2 -3
  12. package/dist/nodefony/service/certificates.js +214 -158
  13. package/dist/nodefony/service/error-renderer.js +1 -1
  14. package/dist/nodefony/service/http-kernel.js +221 -47
  15. package/dist/nodefony/service/reverse-proxy.js +336 -0
  16. package/dist/nodefony/service/servers/server-http.js +3 -3
  17. package/dist/nodefony/service/servers/server-https.js +3 -3
  18. package/dist/nodefony/service/servers/server-static.js +4 -7
  19. package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
  20. package/dist/nodefony/service/servers/server-websocket.js +12 -4
  21. package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
  22. package/dist/nodefony/service/sessions/sessions-service.js +30 -9
  23. package/dist/nodefony/service/trace.js +2 -1
  24. package/dist/nodefony/service/upload/upload-service.js +3 -3
  25. package/dist/nodefony/service/x509.js +324 -0
  26. package/dist/nodefony/src/context/Context.js +28 -1
  27. package/dist/nodefony/src/context/domainMatcher.js +20 -2
  28. package/dist/nodefony/src/context/forwarded.js +2 -2
  29. package/dist/nodefony/src/context/http/HttpContext.js +91 -40
  30. package/dist/nodefony/src/context/http/Request.js +9 -4
  31. package/dist/nodefony/src/context/http/Response.js +46 -7
  32. package/dist/nodefony/src/context/http/SseStream.js +350 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
  34. package/dist/nodefony/src/context/responseEnded.js +27 -0
  35. package/dist/nodefony/src/context/websocket/Response.js +9 -0
  36. package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
  37. package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
  38. package/dist/nodefony/src/profiler/Profiler.js +6 -10
  39. package/dist/nodefony/src/proxy/forward.js +476 -0
  40. package/dist/nodefony/src/proxy/rules.js +182 -0
  41. package/dist/nodefony/src/servers/portBinder.js +57 -2
  42. package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
  43. package/dist/nodefony/src/session/session.js +28 -12
  44. package/dist/types/index.d.ts +5 -0
  45. package/dist/types/nodefony/config/config.d.ts +17 -0
  46. package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
  47. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  48. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  49. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  50. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  52. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  53. package/dist/types/nodefony/service/certificates.d.ts +126 -50
  54. package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
  55. package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
  56. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  57. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
  58. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
  59. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  60. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  61. package/dist/types/nodefony/service/x509.d.ts +86 -0
  62. package/dist/types/nodefony/src/context/Context.d.ts +22 -2
  63. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  64. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
  65. package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
  66. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  67. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  68. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  69. package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
  70. package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
  71. package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
  72. package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
  73. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
  74. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  75. package/docs/cookies.md +13 -13
  76. package/docs/index.md +9 -3
  77. package/docs/observabilite.md +18 -18
  78. package/docs/rate-limit.md +30 -30
  79. package/docs/reverse-proxy.md +522 -0
  80. package/docs/servers.md +115 -78
  81. package/docs/session.md +61 -61
  82. package/docs/sse.md +283 -0
  83. package/docs/upload.md +37 -37
  84. package/package.json +10 -13
@@ -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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Fabrique et lecture de certificats X.509 par `node:crypto` seul — aucune
3
+ * dépendance tierce.
4
+ *
5
+ * `node:crypto` sait LIRE un certificat (`X509Certificate`) et SIGNER, pas
6
+ * en FABRIQUER un : ce module encode le `TBSCertificate` en DER (RFC 5280
7
+ * §4.1), le signe avec `crypto.sign`, puis l'assemble. Seul le sous-ensemble
8
+ * nécessaire à un certificat serveur auto-signé est couvert. Les fonctions
9
+ * sont PURES (aucun disque, aucun kernel) : elles s'éprouvent sans décor.
10
+ *
11
+ * Pourquoi pas node-forge : sa vérification de signature RSA PKCS#1 v1.5
12
+ * accepte des signatures forgées (GHSA-86w9-cpqp-85rv), sans aucune version
13
+ * corrigée. Nous ne vérifions aucune signature, mais `npm audit` signale le
14
+ * paquet pour tous ceux qui installent `@nodefony/http`.
15
+ */
16
+ import { type KeyObject } from "node:crypto";
17
+ /** Hachages de signature que le builder sait déclarer. */
18
+ export type X509Hash = "sha1" | "sha256" | "sha384" | "sha512";
19
+ /**
20
+ * Attribut d'un nom distinctif (sujet / émetteur). Forme reprise de la config
21
+ * historique : `name` long (`commonName`), `shortName` (`CN`) ou `type` (OID
22
+ * pointé) — le premier présent fait foi.
23
+ */
24
+ export interface ICertificateAttribute {
25
+ name?: string | undefined;
26
+ shortName?: string | undefined;
27
+ type?: string | undefined;
28
+ value?: string | undefined;
29
+ }
30
+ /** Ce qu'il faut pour fabriquer un certificat serveur auto-signé. */
31
+ export interface ISelfSignedSpec {
32
+ privateKey: KeyObject;
33
+ publicKey: KeyObject;
34
+ /** Numéro de série en hexadécimal (entier positif, RFC 5280 §4.1.2.2). */
35
+ serialHex: string;
36
+ notBefore: Date;
37
+ notAfter: Date;
38
+ /** Sujet ET émetteur (auto-signé). Vide = nom vide, SAN alors critique. */
39
+ attributes: ICertificateAttribute[];
40
+ dns: string[];
41
+ ip: string[];
42
+ /** `sha1` n'est accepté que pour fabriquer des témoins de test. */
43
+ hash: X509Hash;
44
+ }
45
+ /** OID de la signature RSA-SHA1, que le service régénère. */
46
+ export declare const SHA1_WITH_RSA_OID: string;
47
+ /** Octets d'une adresse IP pour un SAN `iPAddress` (4 ou 16 octets). */
48
+ export declare function ipAddressBytes(ip: string): Buffer;
49
+ /**
50
+ * OID de l'algorithme de signature d'un certificat DER (champ
51
+ * `signatureAlgorithm`, RFC 5280 §4.1.1.2). Lu dans le DER plutôt que par
52
+ * `X509Certificate.signatureAlgorithmOid`, absent des Node du plancher.
53
+ *
54
+ * @param der - certificat encodé DER (`X509Certificate.raw`)
55
+ * @throws Si le DER n'a pas la forme d'un certificat
56
+ */
57
+ export declare function signatureAlgorithmOid(der: Buffer): string;
58
+ /** Nom lisible d'un OID de signature (l'OID lui-même s'il est inconnu). */
59
+ export declare function signatureAlgorithmName(oid: string): string;
60
+ /**
61
+ * Entrées DNS et IP d'un `subjectAltName` tel que le rend
62
+ * `X509Certificate.subjectAltName` (`DNS:a, IP Address:1.2.3.4`).
63
+ */
64
+ export declare function parseSubjectAltName(value: string | undefined): {
65
+ dns: string[];
66
+ ip: string[];
67
+ };
68
+ /**
69
+ * Valeur d'un champ d'un nom distinctif tel que le rend
70
+ * `X509Certificate.subject` / `.issuer` (une ligne `CLÉ=valeur` par champ).
71
+ */
72
+ export declare function distinguishedNameField(dn: string, key: string): string | null;
73
+ /**
74
+ * Fabrique un certificat serveur auto-signé et le rend en PEM.
75
+ *
76
+ * Contenu : version 3, sujet = émetteur, `basicConstraints` cA=false,
77
+ * `keyUsage` (digitalSignature, keyEncipherment — critique),
78
+ * `extKeyUsage` (serverAuth, clientAuth), `subjectAltName` (critique si le
79
+ * sujet est vide, RFC 5280 §4.2.1.6) et `subjectKeyIdentifier` (SHA-1 de la
80
+ * clé publique, méthode 1 de RFC 5280 §4.2.1.2).
81
+ *
82
+ * @param spec - clés, série, validité, noms et hachage
83
+ * @returns le certificat encodé PEM
84
+ * @throws Si un attribut ou une adresse IP est invalide
85
+ */
86
+ export declare function createSelfSignedCertificate(spec: ISelfSignedSpec): Promise<string>;
@@ -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";
@@ -43,7 +43,6 @@ declare class Context extends Service implements IContextInterface {
43
43
  secure: boolean;
44
44
  security?: ISecurityZone | null | undefined;
45
45
  cleaned: boolean;
46
- isControlledAccess: boolean;
47
46
  validDomain: boolean;
48
47
  finished: boolean;
49
48
  pushAllowed: boolean;
@@ -68,6 +67,14 @@ declare class Context extends Service implements IContextInterface {
68
67
  session: Session | null | undefined;
69
68
  cookieSession: Cookie | null | undefined;
70
69
  user: unknown;
70
+ /**
71
+ * Identifiant de l'appelant AUTHENTIFIÉ, posé par le pare-feu ; `null` hors
72
+ * zone ou pour un anonyme. Lisible au démontage, hors de la bulle ALS : c'est
73
+ * ce que lisent le journal d'audit et le profileur. Distinct de `user`, qui
74
+ * lie la session à son principal de SESSION — y écrire l'identité d'un jeton
75
+ * rattacherait la session d'un autre compte.
76
+ */
77
+ userIdentifier: string | null;
71
78
  waitAsync: boolean;
72
79
  isJson: boolean;
73
80
  protected _isHtml: boolean | null;
@@ -116,6 +123,7 @@ declare class Context extends Service implements IContextInterface {
116
123
  private _phaseIndex;
117
124
  private _afterResponseFns;
118
125
  private _afterResponseFired;
126
+ private requestStore;
119
127
  private _abortController;
120
128
  metaData: Data;
121
129
  scheme: SchemeType;
@@ -160,6 +168,18 @@ declare class Context extends Service implements IContextInterface {
160
168
  get signal(): AbortSignal;
161
169
  _abortIfPending(reason?: string): void;
162
170
  log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
171
+ /**
172
+ * Confie au contexte le magasin `RequestContext` de son unité de travail
173
+ * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
174
+ *
175
+ * Un minuteur ou une promesse armé pendant l'unité de travail capture ce
176
+ * magasin et le garde au-delà de la réponse — avec le contexte, l'identité
177
+ * et le jeton du pare-feu. Le vidage ({@link RequestContext.release}) n'y
178
+ * laisse que la corrélation et le scope refermé.
179
+ *
180
+ * @param store - le magasin passé à `RequestContext.run`
181
+ */
182
+ holdRequestStore(store: RequestContextPayload): void;
163
183
  clean(): void;
164
184
  /**
165
185
  * Log d'un event du cycle de vie — gate boot-time (V2.1) : en production le
@@ -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,11 +30,24 @@ 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;
@@ -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;
@@ -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
  /**
@@ -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>;
@@ -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;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
3
+ *
4
+ * Seule construction : le contexte WebSocket en tire son chemin et sa requête,
5
+ * et le contrôle d'avant `101` en tire le chemin qu'il confronte aux routes.
6
+ * Deux constructions divergeraient — `//a/b` lu comme un chemin d'un côté, comme
7
+ * un hôte de l'autre — et une route acceptée par l'une serait refusée par
8
+ * l'autre.
9
+ *
10
+ * @param scheme - `ws` ou `wss`
11
+ * @param host - en-tête `Host` de la requête (`localhost` s'il manque)
12
+ * @param rawUrl - cible brute de la requête (`/` si elle manque)
13
+ * @returns l'URL analysée
14
+ * @throws TypeError si l'hôte ou la cible ne forment pas une URL
15
+ */
16
+ export declare function parseWebsocketUrl(scheme: string, host: string | undefined, rawUrl: string | undefined): URL;
@@ -116,6 +116,7 @@ export interface ProfileSummary {
116
116
  /** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
117
117
  interface ProfilableContext {
118
118
  requestId?: string | undefined;
119
+ userIdentifier?: string | null | undefined;
119
120
  type?: string | undefined;
120
121
  scheme?: string | undefined;
121
122
  method?: string | null | undefined;