@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7

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 (130) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +30 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +29 -4
  7. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  8. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  9. package/dist/nodefony/grpc/index.js +4 -0
  10. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  11. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  12. package/dist/nodefony/service/audit-logger.js +52 -6
  13. package/dist/nodefony/service/error-renderer.js +47 -13
  14. package/dist/nodefony/service/http-kernel.js +306 -49
  15. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  16. package/dist/nodefony/service/request-logger.js +21 -6
  17. package/dist/nodefony/service/reverse-proxy.js +8 -3
  18. package/dist/nodefony/service/servers/clientError.js +53 -4
  19. package/dist/nodefony/service/servers/server-h2c.js +6 -6
  20. package/dist/nodefony/service/servers/server-http.js +6 -4
  21. package/dist/nodefony/service/servers/server-https.js +14 -7
  22. package/dist/nodefony/service/servers/server-static.js +1 -6
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
  24. package/dist/nodefony/service/servers/server-websocket.js +7 -11
  25. package/dist/nodefony/service/servers/serverShutdown.js +43 -6
  26. package/dist/nodefony/service/sessions/sessions-service.js +7 -7
  27. package/dist/nodefony/service/upload/upload-service.js +2 -2
  28. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  29. package/dist/nodefony/src/context/Context.js +12 -1
  30. package/dist/nodefony/src/context/http/ByteStream.js +364 -0
  31. package/dist/nodefony/src/context/http/HttpContext.js +139 -32
  32. package/dist/nodefony/src/context/http/Request.js +111 -22
  33. package/dist/nodefony/src/context/http/Response.js +459 -37
  34. package/dist/nodefony/src/context/http/SseStream.js +83 -148
  35. package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
  36. package/dist/nodefony/src/context/http/parser.js +29 -11
  37. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  38. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  39. package/dist/nodefony/src/context/http2/Request.js +1 -1
  40. package/dist/nodefony/src/context/http2/Response.js +55 -12
  41. package/dist/nodefony/src/context/requestTrace.js +6 -2
  42. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  43. package/dist/nodefony/src/context/transport.js +20 -1
  44. package/dist/nodefony/src/context/websocket/Response.js +15 -3
  45. package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
  46. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  47. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  48. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  49. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  50. package/dist/nodefony/src/cookies/cookie.js +57 -14
  51. package/dist/nodefony/src/errors/httpError.js +58 -25
  52. package/dist/nodefony/src/grpc/cors.js +59 -0
  53. package/dist/nodefony/src/grpc/limits.js +16 -0
  54. package/dist/nodefony/src/grpc/refusal.js +271 -0
  55. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  56. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  57. package/dist/nodefony/src/proxy/forward.js +109 -9
  58. package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
  59. package/dist/nodefony/src/proxy/rules.js +29 -14
  60. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  61. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  62. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  63. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  64. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  65. package/dist/types/index.d.ts +11 -2
  66. package/dist/types/nodefony/config/config.d.ts +14 -0
  67. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  68. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  69. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  70. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  71. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  72. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  73. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  74. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  75. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  76. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  77. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  78. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  79. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  80. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  81. package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
  82. package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
  83. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  84. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  85. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
  86. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  87. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  88. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  89. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
  90. package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
  91. package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
  92. package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
  93. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
  94. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  95. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  96. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  97. package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
  98. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  99. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  100. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  101. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  102. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
  103. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  104. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  105. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  106. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  107. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  108. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  109. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  110. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  111. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  112. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  113. package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
  114. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  115. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  116. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  117. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  118. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  119. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  120. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  121. package/docs/byte-stream.md +406 -0
  122. package/docs/cookies.md +13 -3
  123. package/docs/diagnostics-channel.md +45 -1
  124. package/docs/index.md +8 -0
  125. package/docs/reverse-proxy.md +15 -3
  126. package/docs/servers.md +67 -43
  127. package/docs/session.md +3 -3
  128. package/docs/sse.md +27 -20
  129. package/docs/upload.md +43 -17
  130. package/package.json +38 -18
@@ -0,0 +1,136 @@
1
+ import type http from "node:http";
2
+ /**
3
+ * Refus d'un appel gRPC au format de son protocole (ADR-0026 D5 point 2, D13
4
+ * point 2). Sans Connect : une application sans service gRPC charge ce module
5
+ * avec le noyau, et n'y paie rien tant qu'aucune erreur n'est rendue.
6
+ *
7
+ * Le protocole se reconnaît aux en-têtes de la requête et se code en petit
8
+ * entier (`GRPC_WIRE_*`, 0 = aucun) : une valeur toujours du même type, lue
9
+ * sur le chemin d'erreur seulement.
10
+ */
11
+ /** Requête qui ne parle aucun protocole gRPC. */
12
+ export declare const GRPC_WIRE_NONE = 0;
13
+ /** gRPC natif (`application/grpc`, `application/grpc+proto`…). */
14
+ export declare const GRPC_WIRE_GRPC = 1;
15
+ /** gRPC-Web (`application/grpc-web`, `application/grpc-web-text`…). */
16
+ export declare const GRPC_WIRE_WEB = 2;
17
+ /** Connect unaire (`connect-protocol-version`, ou GET `?connect=v1`). */
18
+ export declare const GRPC_WIRE_CONNECT = 3;
19
+ /** Connect en flux (`application/connect+proto`, `application/connect+json`). */
20
+ export declare const GRPC_WIRE_CONNECT_STREAM = 4;
21
+ /** Le protocole gRPC d'une requête, `GRPC_WIRE_NONE` s'il n'y en a aucun. */
22
+ export type GrpcWire = typeof GRPC_WIRE_NONE | typeof GRPC_WIRE_GRPC | typeof GRPC_WIRE_WEB | typeof GRPC_WIRE_CONNECT | typeof GRPC_WIRE_CONNECT_STREAM;
23
+ /**
24
+ * Reconnaît le protocole gRPC d'une requête à ses en-têtes : `content-type`
25
+ * d'abord, puis `connect-protocol-version`. Une API JSON ordinaire rend
26
+ * `GRPC_WIRE_NONE`.
27
+ *
28
+ * @param headers - les en-têtes de la requête.
29
+ * @param connectGet - `true` pour un GET qui porte `connect=v1` (Connect
30
+ * unaire en GET, sans en-tête propre).
31
+ * @returns le protocole reconnu.
32
+ */
33
+ export declare function grpcWireOf(headers: http.IncomingHttpHeaders, connectGet?: boolean): GrpcWire;
34
+ /** 16 `UNAUTHENTICATED`. */
35
+ export declare const GRPC_UNAUTHENTICATED = 16;
36
+ /** 7 `PERMISSION_DENIED`. */
37
+ export declare const GRPC_PERMISSION_DENIED = 7;
38
+ /**
39
+ * Le nom d'un code gRPC (`unimplemented`, `unauthenticated`…) — le libellé
40
+ * générique d'un refus (ADR-0026 D5 point 4).
41
+ *
42
+ * @param code - le code gRPC, de 0 à 16.
43
+ * @returns son nom ; `unknown` hors de la plage.
44
+ */
45
+ export declare function grpcCodeName(code: number): string;
46
+ /**
47
+ * Le nom d'un code gRPC tel que le publient la ligne d'accès et le canal de
48
+ * traçage (`UNAVAILABLE`) — ADR-0026 D10 point 1.
49
+ *
50
+ * @param code - le code gRPC, de 0 à 16.
51
+ * @returns son nom ; `UNKNOWN` hors de la plage.
52
+ */
53
+ export declare function grpcStatusName(code: number): string;
54
+ /**
55
+ * Le code gRPC d'un refus de transport — rendu avant l'implémentation (chemin
56
+ * inconnu, méthode refusée, limite de débit, pare-feu) — en gRPC natif et en
57
+ * gRPC-Web (ADR-0026 D13 point 2). Distincte de la table des erreurs de
58
+ * l'application (D7) ; seule la règle d'identité de 401/403 est commune (D5).
59
+ *
60
+ * @param status - le statut HTTP que le noyau aurait rendu.
61
+ * @param identified - un appelant authentifié est connu.
62
+ * @returns le code gRPC.
63
+ */
64
+ export declare function transportRefusalCode(status: number, identified: boolean): number;
65
+ /**
66
+ * Le code gRPC d'une erreur levée par l'APPLICATION pendant un appel — une
67
+ * `HttpError` d'un service partagé avec des routes HTTP, une validation, un
68
+ * conflit (ADR-0026 D7, table « HTTP Mapping » de `google/rpc/code.proto`).
69
+ * Distincte de la table des refus de transport ({@link transportRefusalCode}) :
70
+ * un 404 levé par un service dit « entité introuvable » (5), un chemin inconnu
71
+ * dit « méthode absente » (12). Seule la règle d'identité de 401/403 est
72
+ * commune (D5).
73
+ *
74
+ * `code.proto` associe 409 à `ABORTED` et à `ALREADY_EXISTS` : `ABORTED`, le
75
+ * sens le plus large, est retenu ; `ALREADY_EXISTS` se lève en `ConnectError`.
76
+ *
77
+ * @param status - le statut HTTP que rendrait une route HTTP.
78
+ * @param identified - un appelant authentifié est connu.
79
+ * @returns le code gRPC.
80
+ */
81
+ export declare function applicationErrorCode(status: number, identified: boolean): number;
82
+ /**
83
+ * Le code d'un refus en protocole Connect : celui qu'un client DÉDUIT du
84
+ * statut HTTP (`connect-protocol.md`, « HTTP to Error Code »), puisque le
85
+ * statut voyage avec la réponse et doit s'accorder au code. 405 et 421 y
86
+ * valent donc `unknown` (ADR-0026 D13, précision du lot 3) ; 401/403 suivent
87
+ * la règle d'identité (D5).
88
+ *
89
+ * @param status - le statut HTTP du refus.
90
+ * @param identified - un appelant authentifié est connu.
91
+ * @returns le code gRPC.
92
+ */
93
+ export declare function connectRefusalCode(status: number, identified: boolean): number;
94
+ /** Ce que le refus touche de la réponse : son statut et ses en-têtes. */
95
+ export interface IGrpcRefusalTarget {
96
+ setStatusCode(status: number, message?: string): void;
97
+ setHeader(name: string, value: string): void;
98
+ }
99
+ /**
100
+ * Pose le statut et les en-têtes d'un refus au format du protocole, et rend
101
+ * le corps à envoyer — vide en gRPC et gRPC-Web, dont le statut voyage dans
102
+ * les en-têtes (_Trailers-Only_ ; en HTTP/2, `Http2Response.writeHead` termine
103
+ * alors le flux sur la trame d'en-têtes).
104
+ *
105
+ * | Protocole | Statut HTTP | Corps |
106
+ * | --------------- | ------------------------------ | -------------------------------------- |
107
+ * | gRPC, gRPC-Web | 200 | aucun |
108
+ * | Connect unaire | celui du refus (401/403 : D5) | `{"code":"<nom>"}` |
109
+ * | Connect en flux | 200 | message de fin `{"error":{…}}` |
110
+ *
111
+ * Le message n'est jamais celui de l'erreur, qui nomme parfois la zone
112
+ * refusée : seul le nom du code part (D5 point 4).
113
+ *
114
+ * @param response - la réponse du contexte, pas encore envoyée.
115
+ * @param wire - le protocole de la requête, autre que `GRPC_WIRE_NONE`.
116
+ * @param status - le statut HTTP que le noyau aurait rendu.
117
+ * @param identified - un appelant authentifié est connu.
118
+ * @param contentType - le `content-type` de la requête, recopié en gRPC,
119
+ * gRPC-Web et Connect en flux (déjà reconnu par {@link grpcWireOf}).
120
+ * @returns le corps : texte vide, JSON, ou octets d'un message de fin.
121
+ */
122
+ export declare function prepareGrpcRefusal(response: IGrpcRefusalTarget, wire: GrpcWire, status: number, identified: boolean, contentType: string | undefined): string | Uint8Array;
123
+ /**
124
+ * Marque une route comme servie par un point d'entrée gRPC — au démarrage,
125
+ * par `@nodefony/framework/grpc`. Allume la garde qui refuse en 12 une requête
126
+ * gRPC native aboutie à une route qui n'est pas gRPC (ADR-0026 D13 point 3) :
127
+ * tant qu'aucune route n'est marquée, la garde coûte une lecture de booléen.
128
+ *
129
+ * @param route - la route montée pour une méthode du contrat.
130
+ * @experimental Surface gRPC, ADR-0026 D11.
131
+ */
132
+ export declare function markGrpcRoute(route: {
133
+ grpc?: boolean | undefined;
134
+ }): void;
135
+ /** `true` dès qu'une route gRPC est déclarée dans le processus. */
136
+ export declare function hasGrpcRoutes(): boolean;
@@ -116,6 +116,14 @@ export declare class Profiler implements IProfilerReader {
116
116
  private readonly _caps;
117
117
  /** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
118
118
  private _seq;
119
+ /**
120
+ * Démarrage du profileur, donc du processus (ms epoch, horloge du serveur) :
121
+ * aucune requête servie avant n'est connue. Rendu avec un profil absent, pour
122
+ * que le lecteur dise « le serveur a redémarré » plutôt que « évincé ».
123
+ */
124
+ readonly startedAt: number;
125
+ /** Dernier vidage des anneaux (ms epoch), `null` s'il n'y en a jamais eu. */
126
+ private _clearedAt;
119
127
  /**
120
128
  * @param cap - capacité de l'anneau de l'application.
121
129
  * @param sideCap - capacité de chacun des anneaux de l'outillage et des
@@ -155,7 +163,9 @@ export declare class Profiler implements IProfilerReader {
155
163
  since(cursor: number): IProfileBatch;
156
164
  /** Nombre de profils en mémoire. */
157
165
  get size(): number;
158
- /** Vide les anneaux. */
166
+ /** Vide les anneaux, et retient l'instant du vidage ({@link clearedAt}). */
159
167
  clear(): void;
168
+ /** Dernier vidage des anneaux (ms epoch, horloge du serveur), `null` si aucun. */
169
+ get clearedAt(): number | null;
160
170
  }
161
171
  export default Profiler;
@@ -38,6 +38,13 @@ export interface IRelaySettings {
38
38
  */
39
39
  agent: http.Agent | false | undefined;
40
40
  }
41
+ /**
42
+ * En-têtes de CONNEXION (RFC 9110 §7.6.1) : ils décrivent un saut, jamais le
43
+ * message — un intermédiaire ne les transmet pas. Les cinq premiers sont en
44
+ * outre INTERDITS en HTTP/2 (RFC 9113 §8.2.2) : Node lève
45
+ * `ERR_HTTP2_INVALID_CONNECTION_HEADERS` si une réponse les porte.
46
+ */
47
+ export declare const HOP_BY_HOP: ReadonlySet<string>;
41
48
  /**
42
49
  * Sauts `Via` au-delà desquels un message est tenu pour une boucle. Notre
43
50
  * pseudonyme ne repère une boucle qu'au RETOUR dans ce processus : derrière un
@@ -55,6 +62,19 @@ export declare const MAX_VIA_HOPS = 10;
55
62
  * @param pseudonym - pseudonyme de ce processus
56
63
  */
57
64
  export declare function viaLoops(via: string | string[] | undefined, pseudonym: string): boolean;
65
+ /**
66
+ * `Max-Forwards` reçu, tel qu'un intermédiaire doit le lire (RFC 9110 §7.6.2) :
67
+ * sur `OPTIONS` et `TRACE` seulement — la même section permet de l'ignorer
68
+ * pour toute autre méthode, et il part alors tel quel.
69
+ *
70
+ * @param method - méthode de la requête
71
+ * @param value - en-tête `Max-Forwards` reçu
72
+ * @returns `null` s'il n'y a rien à faire (autre méthode, en-tête absent) ;
73
+ * `-1` si la valeur n'est pas `1*DIGIT` — elle ne se décrémente pas, l'en-tête
74
+ * n'est donc pas transmis ; sinon la valeur reçue (`0` : le proxy répond
75
+ * lui-même, sans joindre l'amont).
76
+ */
77
+ export declare function receivedMaxForwards(method: string, value: string | string[] | undefined): number | null;
58
78
  /**
59
79
  * Cadrage du corps transmis à l'amont (RFC 9112 §6). `unsupported` : un
60
80
  * codage de transfert autre que `chunked` seul — le proxy ne sait pas le
@@ -118,8 +138,9 @@ export declare function upstreamPath(url: string, mount: IProxyMount): string;
118
138
  * Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
119
139
  *
120
140
  * Le corps est transmis avec un cadrage cohérent ({@link bodyFramingOf}). Un
121
- * message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Un
122
- * client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
141
+ * message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Sur
142
+ * `OPTIONS` et `TRACE`, `Max-Forwards` est décrémenté ; à `0`, le proxy répond
143
+ * lui-même en destinataire final (RFC 9110 §7.6.2). Un client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
123
144
  * connexion non établie à temps ou un amont muet rendent 504 — à condition
124
145
  * que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
125
146
  * statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
@@ -154,6 +175,20 @@ export declare function rawHead(status: string, rawHeaders: readonly string[], s
154
175
  * @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`…)
155
176
  */
156
177
  export declare function refuseUpgrade(socket: Duplex, status: number, reason: string, headers?: Readonly<Record<string, string | number>> | null): void;
178
+ /**
179
+ * Refuse un handshake WebSocket non conforme — la réponse qui convient à la
180
+ * raison rendue par `websocketHandshakeProblem`, seule implémentation pour le
181
+ * serveur WebSocket ET le proxy inverse.
182
+ *
183
+ * Une version inconnue reçoit `426` avec `Sec-WebSocket-Version: 13` : le
184
+ * client apprend vers quelle version se rabattre (RFC 6455 §4.2.2 point 6,
185
+ * §4.4), et le 426 porte l'`Upgrade` qu'exige RFC 9110 §15.5.22. Tout autre
186
+ * défaut de forme reçoit `400`.
187
+ *
188
+ * @param socket - socket du client, encore en attente de sa réponse
189
+ * @param problem - la raison rendue par `websocketHandshakeProblem`
190
+ */
191
+ export declare function refuseMalformedHandshake(socket: Duplex, problem: string): void;
157
192
  /**
158
193
  * Relaie un upgrade WebSocket DÉJÀ validé (méthode, `Upgrade`, clé, version,
159
194
  * `Origin` — cf `ReverseProxy.handleUpgrade`) : rejoue le handshake vers
@@ -44,8 +44,9 @@ export interface ProxyIntrospection {
44
44
  httpsPort: number;
45
45
  /**
46
46
  * Port h2c du backend (`servers.h2c`), ou `null` s'il n'est pas activé. Le
47
- * générateur haproxy y envoie le trafic en HTTP/2 (`proto h2`) ; nginx, qui
48
- * ne parle pas HTTP/2 à ses amonts, l'ignore.
47
+ * générateur haproxy y envoie tout le trafic hors WebSocket (`proto h2`) ;
48
+ * nginx, qui ne parle HTTP/2 à ses amonts que par `grpc_pass`, n'y envoie
49
+ * que gRPC. Règle d'usage : {@link upstreamH2cPort}.
49
50
  */
50
51
  h2cPort?: number | null;
51
52
  /** Dossiers statiques servis à la racine `/` (ordre = priorité). */
@@ -98,6 +99,18 @@ export interface ProxyIntrospection {
98
99
  }
99
100
  /** Valeurs par défaut d'un modèle d'introspection (complété par la commande). */
100
101
  export declare const defaultIntrospection: ProxyIntrospection;
102
+ /**
103
+ * Port h2c que le proxy doit joindre, ou `null` — règle UNIQUE des deux
104
+ * générateurs.
105
+ *
106
+ * Seulement vers un backend en clair : le re-chiffrement vise le port TLS, qui
107
+ * négocie déjà HTTP/2 par ALPN, et mélanger les deux liens ferait voyager une
108
+ * partie du trafic en clair alors que l'utilisateur a demandé le chiffrement.
109
+ *
110
+ * @param intro - modèle d'introspection Nodefony.
111
+ * @returns le port h2c à joindre, ou `null` s'il n'y a pas de lien h2c.
112
+ */
113
+ export declare function upstreamH2cPort(intro: ProxyIntrospection): number | null;
101
114
  /**
102
115
  * Génère une configuration nginx complète (reverse-proxy + offload statiques).
103
116
  *
@@ -73,6 +73,26 @@ export declare function proxyMountProblems(prefix: string, mount: IProxyMountSha
73
73
  * @returns `true` si le chemin doit être refusé
74
74
  */
75
75
  export declare function isAmbiguousPath(url: string): boolean;
76
+ /**
77
+ * Le corps reçu porte-t-il un codage de transfert que le serveur ne sait pas
78
+ * défaire ? Seul `chunked` est compris : Node le retire, mais laisse en place
79
+ * tout codage qui le précède (`gzip, chunked`) — le corps livré serait encore
80
+ * compressé. RFC 9112 §6.1 : un tel message reçoit 501. Un `chunked` qui n'est
81
+ * pas le DERNIER codage est déjà refusé par Node en 400 (§6.3).
82
+ *
83
+ * Seule règle, pour le serveur d'origine et pour le proxy inverse. Chemin de
84
+ * chaque requête : une lecture d'en-tête, rien d'autre sans `Transfer-Encoding`.
85
+ *
86
+ * @param coding - valeur de l'en-tête `Transfer-Encoding`, absente le plus souvent
87
+ * @returns `true` quand le corps ne peut pas être lu tel qu'annoncé
88
+ */
89
+ export declare function isUnsupportedTransferCoding(coding: string | undefined): boolean;
90
+ /**
91
+ * Raison rendue par {@link websocketHandshakeProblem} quand le client parle une
92
+ * autre version du protocole : la seule qui appelle une réponse distincte —
93
+ * le client doit apprendre vers quelle version se rabattre (RFC 6455 §4.4).
94
+ */
95
+ export declare const WS_UNSUPPORTED_VERSION = "Sec-WebSocket-Version \u2260 13";
76
96
  /**
77
97
  * Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
78
98
  * un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
@@ -0,0 +1,41 @@
1
+ import type { ServerHttp2Session } from "node:http2";
2
+ /** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas la fermeture. */
3
+ export declare const ANNOUNCE_CAP_MS = 1000;
4
+ /**
5
+ * Annonce à UNE session HTTP/2 qu'elle va fermer, SANS écarter un flux déjà en route.
6
+ *
7
+ * Un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
8
+ * aller-retour : après elle, tout flux ouvert par le client avant l'annonce est
9
+ * arrivé, et le GOAWAY définitif (`session.close()`, dernier flux traité) peut
10
+ * suivre. RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last
11
+ * stream identifier set to 2^31-1 […] After allowing time for any in-flight
12
+ * stream creation (at least one round-trip time) ». Sans annonce, un flux croisé
13
+ * avec le GOAWAY est refusé alors qu'il aurait pu être servi.
14
+ *
15
+ * L'aller-retour se MESURE avant l'annonce : après un GOAWAY, un client Node ne
16
+ * répond plus aux PING (constaté sur Node 26.13).
17
+ *
18
+ * @param session - la session à annoncer, ni fermée ni détruite
19
+ * @param settle - appelé UNE fois : après l'attente, à la fermeture de la
20
+ * session, ou aussitôt si le PING ne part pas. Un pair muet ne l'appelle
21
+ * jamais — le délai maximal appartient à l'appelant.
22
+ */
23
+ export declare function announceSessionClose(session: ServerHttp2Session, settle: () => void): void;
24
+ /**
25
+ * Ferme EN DOUCEUR une session HTTP/2 restée inactive au-delà du délai du serveur.
26
+ *
27
+ * RFC 9113 §9.1 : un serveur est « permitted to terminate idle connections »,
28
+ * et « SHOULD first send a GOAWAY ». La fermeture suit l'annonce en deux temps
29
+ * ({@link announceSessionClose}), puis `session.close()` : aucun nouveau flux,
30
+ * mais les flux en cours vont à leur terme — un flux long et muet (abonnement
31
+ * gRPC) n'est pas coupé, seuls les NOUVEAUX flux repartent sur une autre
32
+ * connexion, ce que tout client HTTP/2 fait sur GOAWAY.
33
+ *
34
+ * Le délai de la session est désarmé d'abord : Node (26.13) marque une session
35
+ * détruite AVANT de désarmer son minuteur, qui la retiendrait alors jusqu'à son
36
+ * échéance (nodejs/node#66885).
37
+ *
38
+ * @param session - la session inactive
39
+ * @param capMs - délai maximal d'attente de l'écho du PING (ms)
40
+ */
41
+ export declare function closeIdleSession(session: ServerHttp2Session, capMs?: number): void;
@@ -14,24 +14,37 @@ import type { IHttpConfig } from "../../config/config.js";
14
14
  */
15
15
  export declare function http2ServerLimits(limits: Partial<IHttpConfig["http2"]> | undefined): Pick<ServerOptions, "maxSessionMemory" | "settings">;
16
16
  /**
17
- * Borne l'inactivité des sessions d'un serveur HTTP/2 — TLS (5152) comme h2c —
18
- * et contourne un défaut de Node qui garde chaque session fermée en vie jusqu'à
19
- * l'échéance de ce délai.
17
+ * Ferme les sessions d'un serveur HTTP/2 — TLS (5152) comme h2c — restées
18
+ * inactives au-delà de `timeoutMs`, et contourne un défaut de Node qui garde
19
+ * chaque session fermée en vie jusqu'à l'échéance de ce délai.
20
20
  *
21
- * Le défaut (Node 24, 26) : `closeSession` (`internal/http2/core`) marque la
22
- * session détruite PUIS appelle `session.setTimeout(0)` ; or `setStreamTimeout`
23
- * sort aussitôt sur un objet détruit. Le minuteur n'est jamais désarmé et retient
24
- * la session (~2,4 Ko) pendant `timeoutMs` : sous un flux de connexions neuves,
25
- * connexions/s × délai × 2,4 Ko. Signalé à Node : https://github.com/nodejs/node/issues/66885
21
+ * **La fermeture est À NOUS.** Node arme chaque session par
22
+ * `session.setTimeout(server.timeout, sessionOnTimeout)` — un écouteur `once` —
23
+ * et `sessionOnTimeout` ne détruit la session que si le serveur n'a AUCUN
24
+ * écouteur `timeout` ; s'il en a un, il lui passe la main, UNE fois par session
25
+ * (`internal/http2/core`, Node 26.13). Le serveur 5152 en a toujours un
26
+ * (`routeSocketTimeout`, pour ses sockets HTTP/1.1), et ce délai doit prévenir
27
+ * l'application (`onTimeout`) : sans fermeture ici, une session inactive vivait
28
+ * jusqu'à ce que le CLIENT la ferme (#763). À l'échéance, la session est fermée
29
+ * en douceur — GOAWAY annoncé puis définitif, les flux en cours vont à leur
30
+ * terme ({@link closeIdleSession}, RFC 9113 §9.1 et §6.8).
26
31
  *
27
- * Le contournement : désarmer à la réception du GOAWAY, AVANT la destruction —
28
- * c'est ainsi que ferment un proxy et un client Node, même par `destroy()`. Rien
29
- * n'est retiré à la protection : Node n'agit déjà plus à l'échéance sur une
30
- * session fermée (`sessionOnTimeout` sort sur `closed`). Une coupure TCP sans
31
- * GOAWAY garde la rétention, bornée au délai.
32
+ * **Le contournement** (Node 24, 26) : `closeSession` marque la session détruite
33
+ * PUIS appelle `session.setTimeout(0)`, qui sort aussitôt sur un objet détruit ;
34
+ * le minuteur retient alors la session (~2,4 Ko) pendant `timeoutMs`. Il est
35
+ * désarmé à la réception du GOAWAY, AVANT la destruction — c'est ainsi que
36
+ * ferment un proxy et un client Node, même par `destroy()` ; et
37
+ * `closeIdleSession` le désarme avant de fermer. Signalé à Node :
38
+ * https://github.com/nodejs/node/issues/66885 (corrigé par nodejs/node#66886,
39
+ * absent de la 26.13). Une coupure TCP sans GOAWAY garde la rétention, bornée
40
+ * au délai.
41
+ *
42
+ * Coût par requête : nul — un écouteur par serveur, et la fermeture n'agit qu'à
43
+ * l'échéance.
32
44
  *
33
45
  * @param server - le serveur HTTP/2.
34
46
  * @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
35
- * @param onTimeout - appelé à l'échéance (événement `timeout` du serveur).
47
+ * @param onTimeout - appelé à l'échéance d'une session (événement `timeout` du
48
+ * serveur), après le début de sa fermeture.
36
49
  */
37
50
  export declare function setHttp2SessionTimeout(server: Http2Server | Http2SecureServer, timeoutMs: number | undefined, onTimeout: () => void): void;
@@ -0,0 +1,34 @@
1
+ import { type Http2Stream } from "node:http2";
2
+ /**
3
+ * Ferme en `NO_ERROR` un flux HTTP/2 dont la réponse est partie mais dont le
4
+ * client émet encore, après la grâce d'un aller-retour (`PING`, RFC 9113 §6.7).
5
+ *
6
+ * RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
7
+ * client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. La coupe ne doit
8
+ * jamais précéder l'envoi de la réponse : un `RST_STREAM` soumis pendant que
9
+ * les en-têtes attendent encore dans la file de nghttp2 les fait JETER, et le
10
+ * client reçoit une fin de flux sans statut (un client gRPC la lit INTERNAL).
11
+ * Le `PING` part derrière la réponse ; son écho prouve qu'elle est arrivée.
12
+ *
13
+ * **Un seul `PING` par session** couvre tous les flux qui attendent : un flux
14
+ * arrivé pendant qu'un `PING` est en vol attend le suivant. Le plafond de
15
+ * `PING` en vol de Node (10 par session) n'est donc jamais atteint par ce
16
+ * chemin, et aucun flux n'est coupé sans grâce — ce que faisait la coupe
17
+ * immédiate quand `ping()` rendait `false`, au-delà de 10 refus simultanés.
18
+ *
19
+ * Session en fermeture GRACIEUSE (délai d'inactivité, arrêt) : Node n'envoie
20
+ * plus de `PING`, et la session attend la fin de ses flux — un client muet la
21
+ * retiendrait. La coupe part alors au tour de boucle suivant, le temps que les
22
+ * réponses déjà soumises quittent la file de nghttp2.
23
+ *
24
+ * Après la grâce : client qui a fini d'émettre → rien (le flux se ferme seul,
25
+ * sans `RST_STREAM`) ; sinon → coupé. L'écho d'un `PING` passe avant les autres
26
+ * trames (§6.7) : une trame DATA encore en route peut arriver après lui, la
27
+ * coupe reste conforme (§8.1).
28
+ *
29
+ * Coût : chemin d'erreur seulement — une entrée de `WeakMap` par session tant
30
+ * que des flux attendent, un `PING` par aller-retour et par session.
31
+ *
32
+ * @param stream - flux dont la réponse est ÉCRITE (`finish` passé).
33
+ */
34
+ export declare function closeStreamAfterGrace(stream: Http2Stream): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Une échéance ABSOLUE pour des réceptions en cours, sans un minuteur par
3
+ * réception : un seul balayage périodique pour toutes.
4
+ *
5
+ * ## Pourquoi
6
+ *
7
+ * `node:http2` ne borne pas la réception d'une requête : il n'a que des délais
8
+ * d'INACTIVITÉ (`http2stream.setTimeout`, `http2session.setTimeout`), qu'un
9
+ * client tient en vie avec un octet avant chaque échéance. `requestTimeout`,
10
+ * la borne absolue anti « slow-loris », n'existe que pour HTTP/1.1.
11
+ *
12
+ * ## Le coût, et pourquoi cette forme
13
+ *
14
+ * C'est la forme que Node donne lui-même à `requestTimeout` : aucun minuteur
15
+ * par requête, un contrôle périodique (`connectionsCheckingInterval`). Les
16
+ * réceptions suivies vivent dans un ANNEAU — un tableau des éléments et un
17
+ * `Int32Array` des tours d'entrée, indexés par un numéro de séquence :
18
+ * - suivre = deux écritures ; libérer = une comparaison et une écriture. Ni
19
+ * minuteur, ni fermeture, ni lecture d'horloge (le temps se compte en TOURS),
20
+ * ni champ ajouté à l'élément suivi ;
21
+ * - mesuré contre une `Map` (hachage d'identité de la clé) : ~26–32 ns par
22
+ * réception contre ~90–150 ns, de 1 à 10 000 réceptions simultanées.
23
+ *
24
+ * Toutes les entrées ont le même délai : l'ordre d'insertion EST l'ordre
25
+ * d'échéance. Un tour avance la tête et s'arrête à la première entrée vivante
26
+ * non échue — il ne coûte que ce qu'il expire ou saute.
27
+ *
28
+ * Contrepartie, bornée : une case libérée DERRIÈRE une entrée vivante attend
29
+ * que la tête la dépasse, soit au plus un délai de réception — 12 octets par
30
+ * réception commencée dans cette fenêtre. Sous charge, l'anneau garde sa
31
+ * capacité d'un tour à l'autre (jamais rétréci puis recrû) ; il la rend au
32
+ * premier tour sans réception nouvelle. Le minuteur unique est `unref`, et s'arrête au
33
+ * même moment.
34
+ *
35
+ * ## Précision
36
+ *
37
+ * Une entrée expire entre `timeoutMs` et `timeoutMs + periodMs` après son
38
+ * suivi — jamais AVANT le délai configuré.
39
+ *
40
+ * @typeParam T - l'élément suivi (un contexte de requête)
41
+ */
42
+ export declare class ReceiveDeadline<T extends object> {
43
+ #private;
44
+ /**
45
+ * @param timeoutMs - le délai de réception (> 0)
46
+ * @param onExpire - appelé une fois par entrée échue, déjà retirée
47
+ * @param periodMs - la période du balayage, bornée par le délai (défaut 1 s)
48
+ * @throws RangeError si `timeoutMs` ou `periodMs` n'est pas un entier > 0
49
+ */
50
+ constructor(timeoutMs: number, onExpire: (item: T) => void, periodMs?: number);
51
+ /** Cases occupées, libérées ou non, entre la tête et la queue. */
52
+ get size(): number;
53
+ /** Capacité courante de l'anneau. */
54
+ get capacity(): number;
55
+ /** Vrai tant que le balayage tourne. */
56
+ get running(): boolean;
57
+ /**
58
+ * Suit une réception qui commence ; démarre le balayage s'il dormait.
59
+ *
60
+ * @param item - la réception
61
+ * @returns le numéro à rendre à {@link ReceiveDeadline.release}
62
+ */
63
+ track(item: T): number;
64
+ /**
65
+ * Cesse de suivre une réception terminée (reçue, refusée ou échue). Sans
66
+ * effet si la case a déjà été balayée ou réattribuée : l'identité de
67
+ * l'élément est vérifiée. Le balayage ne s'arrête pas ici mais au tour
68
+ * suivant s'il ne reste rien — jamais un minuteur armé puis désarmé par
69
+ * requête.
70
+ *
71
+ * @param seq - le numéro rendu par {@link ReceiveDeadline.track}
72
+ * @param item - la réception suivie
73
+ */
74
+ release(seq: number, item: T): void;
75
+ /** Arrête le balayage et oublie toutes les réceptions (arrêt du serveur). */
76
+ close(): void;
77
+ }
@@ -1,6 +1,6 @@
1
1
  import { type IncomingMessage } from "node:http";
2
2
  import type { Duplex } from "node:stream";
3
- import type { WebSocket, WebSocketServer } from "ws";
3
+ import { WebSocketServer, type ServerOptions, type WebSocket } from "ws";
4
4
  /** Serveur qui émet `upgrade` : HTTP/1.1, HTTPS ou HTTP/2 sécurisé (`allowHTTP1`). */
5
5
  interface IUpgradeEmitter {
6
6
  on(event: "upgrade", listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): unknown;
@@ -26,6 +26,18 @@ export interface IUpgradeAdmission {
26
26
  * le handshake) — il n'y a alors rien à servir
27
27
  */
28
28
  accept(): WebSocket | null | Promise<WebSocket | null>;
29
+ /**
30
+ * Retient le sous-protocole que le serveur renverra dans le `101` (RFC 6455
31
+ * §4.2.2 : « the subprotocol the server is ready to use »), à appeler AVANT
32
+ * {@link accept}. Sans choix, la règle d'avant s'applique : l'option
33
+ * `handleProtocols` de la configuration `ws`, sinon le premier offert.
34
+ *
35
+ * Un choix absent de l'offre du client n'est jamais renvoyé — la norme ne
36
+ * permet que l'un des sous-protocoles offerts, ou aucun.
37
+ *
38
+ * @param protocol - le sous-protocole choisi, `false` pour n'en renvoyer aucun
39
+ */
40
+ selectSubprotocol(protocol: string | false): void;
29
41
  /**
30
42
  * Refuse par un statut HTTP, AVANT le `101` (RFC 6455 §4.2.2).
31
43
  *
@@ -36,6 +48,34 @@ export interface IUpgradeAdmission {
36
48
  }
37
49
  /** Ce qui décide d'un upgrade : reçoit la requête et rend son verdict, plus tard s'il le faut. */
38
50
  export type UpgradeAdmit = (req: IncomingMessage, admission: IUpgradeAdmission) => void;
51
+ /** Signature de l'option `handleProtocols` de `ws` (`ServerOptions`). */
52
+ export type SubprotocolHandler = (protocols: Set<string>, request: IncomingMessage) => string | false;
53
+ /**
54
+ * Fabrique l'option `handleProtocols` du serveur `ws` de Nodefony : le choix
55
+ * retenu par l'admission ({@link IUpgradeAdmission.selectSubprotocol}) s'il y
56
+ * en a un, sinon `fallback` (celle de la configuration), sinon le premier
57
+ * offert — le défaut de `ws` 8, conservé pour tout contrôleur qui ne choisit
58
+ * pas.
59
+ *
60
+ * @param fallback - l'option `handleProtocols` de la configuration, s'il y en a une
61
+ * @returns le rappel à passer à `new WebSocketServer({ handleProtocols })`
62
+ */
63
+ export declare function createSubprotocolHandler(fallback?: SubprotocolHandler): SubprotocolHandler;
64
+ /**
65
+ * Construit le serveur `ws` de Nodefony à partir des options de la
66
+ * configuration, en forçant ce que Nodefony gère — seul endroit où ces
67
+ * options sont posées, pour le serveur clair comme pour le sécurisé :
68
+ *
69
+ * - `noServer` : l'upgrade passe d'abord par le proxy inverse, puis par
70
+ * {@link attachUpgradeDispatch} ;
71
+ * - `clientTracking` : requis par `broadcast()` et le heartbeat ;
72
+ * - `handleProtocols` : le sous-protocole CHOISI par le contrôleur
73
+ * ({@link createSubprotocolHandler}), celui de la configuration à défaut.
74
+ *
75
+ * @param options - options `ws` issues de la configuration du serveur WebSocket
76
+ * @returns le serveur, sans écoute propre
77
+ */
78
+ export declare function createNodefonyWebSocketServer(options: ServerOptions): WebSocketServer;
39
79
  /**
40
80
  * Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
41
81
  * puis le serveur WebSocket de Nodefony.