@nodefony/http 10.0.0-beta.5 → 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.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +4 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +112 -34
- package/dist/nodefony/service/pretty-request-logger.js +1 -1
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +55 -26
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +8 -4
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +71 -14
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +6 -0
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +15 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +45 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +5 -3
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +40 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
|
@@ -36,6 +36,46 @@ export interface ISplitTarget {
|
|
|
36
36
|
* @returns les composants découpés, ou `null` (bail-out → vrai `new URL`).
|
|
37
37
|
*/
|
|
38
38
|
export declare function splitTarget(target: unknown): ISplitTarget | null;
|
|
39
|
+
/** Un request-target en forme absolue, découpé en autorité et forme origine. */
|
|
40
|
+
export interface IAbsoluteTarget {
|
|
41
|
+
/** `hôte[:port]` de la cible — l'hôte de la requête (RFC 9112 §3.2.2). */
|
|
42
|
+
authority: string;
|
|
43
|
+
/** La forme origine (`/chemin[?query]`), `/` quand la cible n'a pas de chemin. */
|
|
44
|
+
target: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Découpe un request-target en forme absolue (`http://hôte:port/chemin?q`) :
|
|
48
|
+
* la forme qu'un client envoie à un proxy, et qu'un serveur d'origine DOIT
|
|
49
|
+
* accepter (RFC 9112 §3.2.2). Il DOIT alors ignorer l'en-tête `Host` et prendre
|
|
50
|
+
* l'hôte de la cible — le recoller à `Host` fabriquait un hôte qui n'existe pas.
|
|
51
|
+
*
|
|
52
|
+
* Seuls `http` et `https` sont reconnus (RFC 6455 §4.2.1 vaut aussi pour un
|
|
53
|
+
* handshake WebSocket) ; le schéma de la cible ne remplace jamais celui de la
|
|
54
|
+
* connexion. Une autorité porteuse d'identifiants (`user@hôte`, interdite par
|
|
55
|
+
* RFC 9110 §4.2.4) n'est pas reconnue : `Host` garde la main.
|
|
56
|
+
*
|
|
57
|
+
* Chemin de chaque requête : un seul test de caractère pour la forme origine,
|
|
58
|
+
* aucune allocation.
|
|
59
|
+
*
|
|
60
|
+
* @param target - le request-target brut, tel que reçu.
|
|
61
|
+
* @returns l'autorité et la forme origine, ou `null` pour toute autre forme.
|
|
62
|
+
*/
|
|
63
|
+
export declare function splitAbsoluteForm(target: string | undefined): IAbsoluteTarget | null;
|
|
64
|
+
/**
|
|
65
|
+
* Analyse un request-target origin-form (`/chemin[?query]`) dans la forme
|
|
66
|
+
* NORMALISÉE que la porte HTTP donne au routeur (`HttpRequest`) : découpe exacte
|
|
67
|
+
* quand le parse WHATWG serait l'identité ({@link splitTarget}), vrai parse
|
|
68
|
+
* WHATWG sinon — dot-segments résolus, `\` replié, percent-encoding posé.
|
|
69
|
+
*
|
|
70
|
+
* Pour qui reçoit un chemin hors d'une requête HTTP — le pont WebSocket
|
|
71
|
+
* `api.request` — et doit router exactement comme HTTP : la même chaîne, deux
|
|
72
|
+
* transports, une seule forme.
|
|
73
|
+
*
|
|
74
|
+
* @param target - le chemin reçu ; il doit commencer par `/`.
|
|
75
|
+
* @returns pathname et search normalisés, ou `null` si `target` n'est pas un
|
|
76
|
+
* chemin origin-form analysable.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseRequestTarget(target: string): ISplitTarget | null;
|
|
39
79
|
/**
|
|
40
80
|
* L'autorité (`host[:port]` brut de l'en-tête `Host` / `:authority`) est-elle
|
|
41
81
|
* déjà sous la forme exacte que le parseur WHATWG rendrait ?
|
|
@@ -9,8 +9,9 @@ interface IHeaderSource {
|
|
|
9
9
|
getHeader(name: string): http.OutgoingHttpHeader | undefined;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Compose les en-têtes d'une réponse HTTP/2 : ceux
|
|
13
|
-
* ceux
|
|
12
|
+
* Compose les en-têtes d'une réponse HTTP/2 : ceux qu'un tiers a écrits sur la
|
|
13
|
+
* réponse Node, puis ceux de la réserve de la réponse, puis ceux passés à
|
|
14
|
+
* `writeHead` — chacun l'emporte sur le précédent.
|
|
14
15
|
*
|
|
15
16
|
* Les clés sont ramenées en MINUSCULES : `getHeaders()` rend `content-type`,
|
|
16
17
|
* un contrôleur passe `Content-Type` — fusionnées telles quelles, les deux
|
|
@@ -26,9 +27,10 @@ interface IHeaderSource {
|
|
|
26
27
|
* @param statusMessage - message de statut, recopié en `x-status-message`
|
|
27
28
|
* @param current - la réponse Node qui porte les en-têtes déjà posés
|
|
28
29
|
* @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
|
|
30
|
+
* @param stored - la réserve de la réponse, liste plate aux noms en minuscules
|
|
29
31
|
* @returns les en-têtes à transmettre à `respond()`
|
|
30
32
|
*/
|
|
31
|
-
export declare function mergeResponseHeaders(statusMessage: string, current: IHeaderSource, extra?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): http.OutgoingHttpHeaders;
|
|
33
|
+
export declare function mergeResponseHeaders(statusMessage: string, current: IHeaderSource, extra?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[], stored?: readonly http.OutgoingHttpHeader[]): http.OutgoingHttpHeaders;
|
|
32
34
|
declare class Http2Response extends HttpResponse {
|
|
33
35
|
statusCode: number;
|
|
34
36
|
stream: http2.ServerHttp2Stream | null;
|
|
@@ -51,3 +51,20 @@ export declare function requestAuthority(headers: {
|
|
|
51
51
|
host?: string | undefined;
|
|
52
52
|
":authority"?: string | undefined;
|
|
53
53
|
}): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Une requête HTTP/2 nomme-t-elle deux hôtes — `:authority` ET un `Host`
|
|
56
|
+
* différent ? RFC 9113 §8.3.1 : « SHOULD treat a request as malformed ». Le
|
|
57
|
+
* serveur lit `:authority` ({@link requestAuthority}), mais un code qui lit
|
|
58
|
+
* `headers.host` verrait un autre hôte que celui que le routage et le
|
|
59
|
+
* pare-feu ont jugé.
|
|
60
|
+
*
|
|
61
|
+
* Chemin de chaque requête : une lecture d'en-tête ; la comparaison n'a lieu
|
|
62
|
+
* que si les deux sont présents (un navigateur n'envoie pas `Host` en HTTP/2).
|
|
63
|
+
*
|
|
64
|
+
* @param headers - en-têtes de la requête Node.
|
|
65
|
+
* @returns `true` quand les deux autorités diffèrent (casse ignorée).
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasConflictingAuthority(headers: {
|
|
68
|
+
host?: string | undefined;
|
|
69
|
+
":authority"?: string | undefined;
|
|
70
|
+
}): boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Context from "../Context.js";
|
|
2
2
|
import { ServerType, SchemeType } from "../../../service/http-kernel.js";
|
|
3
3
|
import { Severity, Msgid, Message, nodefonyError, Scope } from "nodefony";
|
|
4
|
+
import type { IFrameCodec } from "nodefony";
|
|
4
5
|
import Ws from "ws";
|
|
5
6
|
import type { IncomingMessage } from "node:http";
|
|
6
7
|
import WebsocketResponse from "./Response.js";
|
|
@@ -11,6 +12,7 @@ import HttpError from "../../errors/httpError.js";
|
|
|
11
12
|
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
12
13
|
import type { PhaseName, IWebsocketContext } from "../../../interfaces/IContext.js";
|
|
13
14
|
import { ProxyType } from "../http/HttpContext.js";
|
|
15
|
+
import { type IFrameRateWindow } from "./wsFrameRate.js";
|
|
14
16
|
import { type ResolvedProxy } from "../forwarded.js";
|
|
15
17
|
export interface IWsRequestExtension {
|
|
16
18
|
url: URL;
|
|
@@ -70,10 +72,22 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
|
|
|
70
72
|
request: WsIncomingMessage | null;
|
|
71
73
|
response: WebsocketResponse | null;
|
|
72
74
|
acceptedProtocol?: string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Codec des trames BINAIRES de cette connexion, posé par la couche qui l'a négocié
|
|
77
|
+
* (le temps réel, ADR-0022 §2). Hors production, le traceur s'en sert pour
|
|
78
|
+
* afficher une trame binaire décodée plutôt que sa seule taille ; jamais lu en
|
|
79
|
+
* production. `null` = octets bruts, résumés à leur taille.
|
|
80
|
+
*/
|
|
81
|
+
frameCodec: IFrameCodec | null;
|
|
73
82
|
port: number | string;
|
|
74
83
|
rejected: boolean;
|
|
75
84
|
teardownWired: boolean;
|
|
76
85
|
connection: Ws | null;
|
|
86
|
+
/**
|
|
87
|
+
* Fenêtre de la borne de débit entrante — `null` tant que la borne est
|
|
88
|
+
* désactivée ou qu'aucune trame n'est arrivée (créée au premier usage).
|
|
89
|
+
*/
|
|
90
|
+
frameRate: IFrameRateWindow | null;
|
|
77
91
|
/**
|
|
78
92
|
* Le verdict du handshake encore à rendre : posé par le noyau tant que le
|
|
79
93
|
* pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
|
|
@@ -110,6 +124,22 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
|
|
|
110
124
|
render(chunk: unknown, encoding?: BufferEncoding): Promise<WebsocketResponse>;
|
|
111
125
|
send(data?: string | Buffer | null, encoding?: BufferEncoding): Promise<WebsocketResponse>;
|
|
112
126
|
broadcast(data?: string | Buffer | null, encoding?: BufferEncoding): null | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Le contenu des messages est-il journalisé ? Hors production seulement —
|
|
129
|
+
* décidé une fois pour le processus (P8 : runtime ∈ {development, production},
|
|
130
|
+
* `resolveRuntimeEnv`).
|
|
131
|
+
*/
|
|
132
|
+
private tracesMessageContent;
|
|
133
|
+
/**
|
|
134
|
+
* Traceur des trames SORTANTES écrites hors de {@link send} — le temps réel
|
|
135
|
+
* écrit droit sur la socket. À demander UNE fois par connexion : `null` en
|
|
136
|
+
* production, donc l'appelant choisit son chemin d'écriture à l'ouverture et
|
|
137
|
+
* ne paie rien par trame.
|
|
138
|
+
*
|
|
139
|
+
* @returns une fonction qui journalise une trame comme `send` (`WS SEND`),
|
|
140
|
+
* ou `null` quand le contenu des messages n'est pas journalisé.
|
|
141
|
+
*/
|
|
142
|
+
outboundTracer(): ((data: unknown) => void) | null;
|
|
113
143
|
/**
|
|
114
144
|
* Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
|
|
115
145
|
* gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
|
|
@@ -150,6 +180,16 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
|
|
|
150
180
|
*/
|
|
151
181
|
phaseStart(name: PhaseName): void;
|
|
152
182
|
phaseEnd(name: PhaseName): void;
|
|
183
|
+
/**
|
|
184
|
+
* Applique la borne de débit entrante à une trame : `true` = l'admettre.
|
|
185
|
+
*
|
|
186
|
+
* Écartée → comptée (`frameRateRejectedTotal`) ; solde de refus atteint →
|
|
187
|
+
* connexion fermée en `1008` (RFC 6455 §7.4.1, violation de politique).
|
|
188
|
+
*
|
|
189
|
+
* @param bound - trames admises par seconde (`> 0`)
|
|
190
|
+
* @returns `true` si la trame poursuit son chemin
|
|
191
|
+
*/
|
|
192
|
+
private admitInboundFrame;
|
|
153
193
|
handleMessage(data: Buffer | string, isBinary: boolean): Promise<unknown>;
|
|
154
194
|
onClose(code: number, reason: Buffer): void;
|
|
155
195
|
/**
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* un hôte de l'autre — et une route acceptée par l'une serait refusée par
|
|
8
8
|
* l'autre.
|
|
9
9
|
*
|
|
10
|
+
* Une cible en forme absolue (`http://hôte/chemin`, RFC 6455 §4.2.1) donne son
|
|
11
|
+
* hôte, et l'en-tête `Host` est ignoré (RFC 9112 §3.2.2) — la même règle que
|
|
12
|
+
* `HttpRequest.getHost`.
|
|
13
|
+
*
|
|
10
14
|
* @param scheme - `ws` ou `wss`
|
|
11
15
|
* @param host - en-tête `Host` de la requête (`localhost` s'il manque)
|
|
12
16
|
* @param rawUrl - cible brute de la requête (`/` si elle manque)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Ws, { WebSocketServer } from "ws";
|
|
2
|
+
import { type BackpressurePolicy, type BackpressureVerdict } from "nodefony";
|
|
2
3
|
/** Politique appliquée quand le buffer d'envoi d'un client dépasse le seuil. */
|
|
3
|
-
export type WsBackpressurePolicy =
|
|
4
|
+
export type WsBackpressurePolicy = BackpressurePolicy;
|
|
4
5
|
/**
|
|
5
6
|
* Options de backpressure SORTANTE (serveur → client).
|
|
6
7
|
*
|
|
@@ -24,6 +25,10 @@ export interface IWsBackpressureOptions {
|
|
|
24
25
|
export interface IBackpressureTarget {
|
|
25
26
|
readonly bufferedAmount?: number;
|
|
26
27
|
close(code?: number, reason?: string): void;
|
|
28
|
+
/** Cumul des frames refusées pour contre-pression (lazy — lu par la sonde socket). */
|
|
29
|
+
_nfDrops?: number;
|
|
30
|
+
/** Solde de refus courant (cf {@link nextDropBalance}) ; absent = 0. */
|
|
31
|
+
_nfDropStreak?: number;
|
|
27
32
|
}
|
|
28
33
|
/** Socket augmentée d'un compteur de drops (number lazy — lu par la sonde socket). */
|
|
29
34
|
export interface IBackpressureSocket extends Ws {
|
|
@@ -37,7 +42,7 @@ export interface IBackpressureSocket extends Ws {
|
|
|
37
42
|
_nfDropStreak?: number;
|
|
38
43
|
}
|
|
39
44
|
/** Décision pour une frame : l'émettre, la sauter, ou (socket fermée) ne rien faire. */
|
|
40
|
-
export type WsSendDecision =
|
|
45
|
+
export type WsSendDecision = BackpressureVerdict;
|
|
41
46
|
/**
|
|
42
47
|
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
43
48
|
*
|
|
@@ -65,9 +70,14 @@ export declare const readBackpressureOptions: (wss: WebSocketServer | null | und
|
|
|
65
70
|
* « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
|
|
66
71
|
* — le caller émet uniquement si le retour vaut `"send"`.
|
|
67
72
|
*
|
|
73
|
+
* La DÉCISION est celle du cœur (`decideBackpressure` / `nextDropBalance`, partagée
|
|
74
|
+
* avec `NodefonySocket`) ; cette fonction n'en porte que l'état (compteurs sur la
|
|
75
|
+
* socket) et le geste serveur (fermeture `1013`).
|
|
76
|
+
*
|
|
68
77
|
* @param ws - la socket destinataire
|
|
69
78
|
* @param max - seuil d'octets (`<= 0` → désactivé)
|
|
70
79
|
* @param policy - action au dépassement
|
|
80
|
+
* @param closeAfterDrops - solde de refus qui ferme (`0` = jamais)
|
|
71
81
|
* @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
|
|
72
82
|
*/
|
|
73
83
|
export declare const decideSend: (ws: IBackpressureTarget, max: number, policy: WsBackpressurePolicy, closeAfterDrops?: number) => WsSendDecision;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type BackpressureVerdict } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Options de la borne de débit ENTRANTE (client → serveur), lues dans `wss.options`.
|
|
4
|
+
*
|
|
5
|
+
* Knobs Nodefony (pas des options `ws`), conservés par le spread du constructeur
|
|
6
|
+
* de `ws` — même voie que `IWsBackpressureOptions`.
|
|
7
|
+
*/
|
|
8
|
+
export interface IWsFrameRateOptions {
|
|
9
|
+
/** Trames reçues admises par seconde et par connexion ; `0`/absent = désactivée. */
|
|
10
|
+
maxFramesPerSecond?: number;
|
|
11
|
+
/** Solde de trames écartées qui ferme la connexion (`1008`) ; `0` = jamais. */
|
|
12
|
+
frameRateCloseAfterRejects?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fenêtre de comptage d'une connexion, créée à la PREMIÈRE trame reçue borne activée.
|
|
16
|
+
*
|
|
17
|
+
* Mutée en place : aucune allocation par trame.
|
|
18
|
+
*/
|
|
19
|
+
export interface IFrameRateWindow {
|
|
20
|
+
/** Début de la fenêtre courante (ms, `Date.now()`). */
|
|
21
|
+
start: number;
|
|
22
|
+
/** Trames reçues dans la fenêtre courante. */
|
|
23
|
+
count: number;
|
|
24
|
+
/** Solde de refus (+1 par trame écartée, −1 par trame admise). */
|
|
25
|
+
balance: number;
|
|
26
|
+
}
|
|
27
|
+
/** Durée d'une fenêtre de comptage (ms) : la borne s'exprime en trames PAR SECONDE. */
|
|
28
|
+
export declare const FRAME_RATE_WINDOW_MS = 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Nombre cumulé de trames entrantes écartées par la borne de débit dans ce processus (unité : une trame reçue).
|
|
31
|
+
*
|
|
32
|
+
* Monotone, `0` tant que la borne est désactivée. Relayé par la sonde du hub
|
|
33
|
+
* temps réel sous le nom figé `frameRateRejectedTotal` (ADR-0021 §S9).
|
|
34
|
+
*
|
|
35
|
+
* @returns le cumul depuis le démarrage du processus
|
|
36
|
+
*/
|
|
37
|
+
export declare const frameRateRejectedTotal: () => number;
|
|
38
|
+
/**
|
|
39
|
+
* Borne de débit entrante du serveur WebSocket (`maxFramesPerSecond`) ; `0` = désactivée.
|
|
40
|
+
*
|
|
41
|
+
* Appelée à chaque trame : une lecture de propriété, aucune allocation.
|
|
42
|
+
*
|
|
43
|
+
* @param server - le serveur du contexte (`context.server`)
|
|
44
|
+
* @returns trames admises par seconde, `0` si la borne est désactivée
|
|
45
|
+
*/
|
|
46
|
+
export declare const readFrameRateBound: (server: unknown) => number;
|
|
47
|
+
/**
|
|
48
|
+
* Solde de refus qui ferme la connexion (`frameRateCloseAfterRejects`) ; `0` = jamais.
|
|
49
|
+
*
|
|
50
|
+
* @param server - le serveur du contexte (`context.server`)
|
|
51
|
+
* @returns le solde de fermeture
|
|
52
|
+
*/
|
|
53
|
+
export declare const readFrameRateCloseAfter: (server: unknown) => number;
|
|
54
|
+
/**
|
|
55
|
+
* Décide du sort d'une trame reçue, AVANT tout décodage : l'admettre, l'écarter, ou fermer la connexion.
|
|
56
|
+
*
|
|
57
|
+
* Compte des TRAMES, pas des octets (`maxPayload` borne déjà la taille). La
|
|
58
|
+
* décision est la règle unique du cœur (`decideBackpressure`), appliquée au
|
|
59
|
+
* nombre de trames de la fenêtre : écarter au-delà de la borne, fermer quand le
|
|
60
|
+
* solde de refus atteint `closeAfter` — jamais au premier dépassement. Une
|
|
61
|
+
* trame écartée est comptée dans {@link frameRateRejectedTotal}.
|
|
62
|
+
*
|
|
63
|
+
* @param win - la fenêtre de la connexion (mutée)
|
|
64
|
+
* @param now - l'instant de réception (ms)
|
|
65
|
+
* @param bound - trames admises par seconde (`> 0`)
|
|
66
|
+
* @param closeAfter - solde de refus qui ferme (`0` = jamais)
|
|
67
|
+
* @returns `"send"` (admettre), `"drop"` (écarter) ou `"close"` (fermer en `1008`)
|
|
68
|
+
*/
|
|
69
|
+
export declare const admitFrame: (win: IFrameRateWindow, now: number, bound: number, closeAfter: number) => BackpressureVerdict;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Formatage **pur** (0 état
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Formatage **pur** (0 état) du CONTENU d'un message WebSocket pour le log de trace
|
|
3
|
+
* (Suivi de requête Studio). Séparé de `WebsocketContext` pour être testable aux
|
|
4
|
+
* limites sans serveur.
|
|
5
5
|
*
|
|
6
6
|
* 🔒 Robustesse binaire (cf doc `ws` — `socket.send(data)` accepte
|
|
7
7
|
* `String | Number | Object | Buffer | ArrayBuffer | TypedArray | DataView |
|
|
8
8
|
* Buffer[] | Blob`, et l'event `message` livre `Buffer | ArrayBuffer | Buffer[]`).
|
|
9
|
-
* **Toute** charge binaire est résumée `[binary N B]` — JAMAIS
|
|
10
|
-
* (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme
|
|
11
|
-
* faux).
|
|
9
|
+
* **Toute** charge binaire est résumée `[binary N B]` — JAMAIS vidée octet par
|
|
10
|
+
* octet (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme
|
|
11
|
+
* et faux). Quand la connexion a négocié un codec de frame (CBOR, ADR-0022 §2), la
|
|
12
|
+
* trame est en plus DÉCODÉE pour être lisible : le débogage se fait à l'observation,
|
|
13
|
+
* jamais en changeant le fil.
|
|
12
14
|
*/
|
|
15
|
+
import { type IFrameCodec } from "nodefony";
|
|
13
16
|
/** Cap de troncature du contenu loggé (octets/chars). Borne ring + JSONL. */
|
|
14
17
|
export declare const WS_LOG_CONTENT_CAP = 4096;
|
|
15
18
|
/**
|
|
@@ -26,12 +29,17 @@ export declare function binaryByteLength(data: unknown): number;
|
|
|
26
29
|
/**
|
|
27
30
|
* Formate une charge utile WS en chaîne **bornée** et **sûre** pour le log :
|
|
28
31
|
* - `string` → tronquée à `cap` (+ ellipse) ;
|
|
29
|
-
* - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de
|
|
32
|
+
* - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de vidage) ;
|
|
33
|
+
* avec un `codec`, une trame d'un seul tenant est aussi décodée :
|
|
34
|
+
* `[binary N B] <protocole> <frame en JSON>`, octets imbriqués étiquetés ;
|
|
35
|
+
* illisible → `[binary N B] <protocole> illisible` ;
|
|
30
36
|
* - `null`/`undefined` → `""` ;
|
|
31
37
|
* - objet « JSON » → `JSON.stringify` compact tronqué (cycle, `bigint`, valeur
|
|
32
38
|
* non sérialisable → repli `String(...)`).
|
|
33
39
|
*
|
|
34
40
|
* @param data - charge utile (RECEIVE/SEND/BROADCAST).
|
|
35
41
|
* @param cap - longueur max avant troncature (défaut {@link WS_LOG_CONTENT_CAP}).
|
|
42
|
+
* @param codec - codec des trames binaires de la connexion, `null` = octets bruts.
|
|
43
|
+
* @returns la ligne à journaliser, jamais plus longue que `cap` + 1.
|
|
36
44
|
*/
|
|
37
|
-
export declare function formatWsLogContent(data: unknown, cap?: number): string;
|
|
45
|
+
export declare function formatWsLogContent(data: unknown, cap?: number, codec?: IFrameCodec | null): string;
|
|
@@ -19,7 +19,6 @@ declare module "http2" {
|
|
|
19
19
|
* externe nomme le type par `ICookieOptions`, exporté au barrel.
|
|
20
20
|
*/
|
|
21
21
|
export type CookieOptionsType = ICookieOptions;
|
|
22
|
-
declare function parser(strToParse: string): import("cookie").Cookies;
|
|
23
22
|
declare function cookiesParser(context: ContextType): void;
|
|
24
23
|
declare class Cookie implements ICookieInterface {
|
|
25
24
|
options: CookieOptionsType;
|
|
@@ -57,7 +56,26 @@ declare class Cookie implements ICookieInterface {
|
|
|
57
56
|
setExpires(date: Date | string | number | undefined): Date | undefined;
|
|
58
57
|
setOriginalMaxAge(ms: number | StringValue): number;
|
|
59
58
|
setPriority(val: PriorityType): PriorityType;
|
|
59
|
+
/**
|
|
60
|
+
* Durée de vie restante en SECONDES ENTIÈRES, `undefined` sous une seconde.
|
|
61
|
+
*
|
|
62
|
+
* RFC 6265 §4.1.1 : `max-age-av = "Max-Age=" non-zero-digit *DIGIT`. Un
|
|
63
|
+
* `Max-Age=3599.874` est ignoré par le navigateur, et le cookie devient un
|
|
64
|
+
* cookie de session. Arrondi INFÉRIEUR : le cookie ne survit jamais à son
|
|
65
|
+
* `Expires`.
|
|
66
|
+
*/
|
|
60
67
|
getMaxAge(): number | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Vérifie la grammaire RFC 6265 §4.1.1 du nom, du `Path` et du `Domain`,
|
|
70
|
+
* au moment où le cookie est POSÉ sur la réponse.
|
|
71
|
+
*
|
|
72
|
+
* Sans elle, `path: "/; Domain=evil.example"` injectait un attribut, et un
|
|
73
|
+
* nom porteur de `;` partait tel quel. La grammaire est celle du paquet
|
|
74
|
+
* `cookie` : une seule implémentation.
|
|
75
|
+
*
|
|
76
|
+
* @throws {TypeError} nommant l'attribut hors grammaire.
|
|
77
|
+
*/
|
|
78
|
+
assertGrammar(): void;
|
|
61
79
|
toString(): string;
|
|
62
80
|
/**
|
|
63
81
|
* Signe une valeur de cookie : `HMAC-SHA256(value)` clé = `secret`, encodé en
|
|
@@ -85,4 +103,4 @@ declare class Cookie implements ICookieInterface {
|
|
|
85
103
|
serializeWebSocket(): IWsCookie;
|
|
86
104
|
}
|
|
87
105
|
export default Cookie;
|
|
88
|
-
export { cookiesParser
|
|
106
|
+
export { cookiesParser };
|
|
@@ -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;
|
|
@@ -55,6 +55,19 @@ export declare const MAX_VIA_HOPS = 10;
|
|
|
55
55
|
* @param pseudonym - pseudonyme de ce processus
|
|
56
56
|
*/
|
|
57
57
|
export declare function viaLoops(via: string | string[] | undefined, pseudonym: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* `Max-Forwards` reçu, tel qu'un intermédiaire doit le lire (RFC 9110 §7.6.2) :
|
|
60
|
+
* sur `OPTIONS` et `TRACE` seulement — la même section permet de l'ignorer
|
|
61
|
+
* pour toute autre méthode, et il part alors tel quel.
|
|
62
|
+
*
|
|
63
|
+
* @param method - méthode de la requête
|
|
64
|
+
* @param value - en-tête `Max-Forwards` reçu
|
|
65
|
+
* @returns `null` s'il n'y a rien à faire (autre méthode, en-tête absent) ;
|
|
66
|
+
* `-1` si la valeur n'est pas `1*DIGIT` — elle ne se décrémente pas, l'en-tête
|
|
67
|
+
* n'est donc pas transmis ; sinon la valeur reçue (`0` : le proxy répond
|
|
68
|
+
* lui-même, sans joindre l'amont).
|
|
69
|
+
*/
|
|
70
|
+
export declare function receivedMaxForwards(method: string, value: string | string[] | undefined): number | null;
|
|
58
71
|
/**
|
|
59
72
|
* Cadrage du corps transmis à l'amont (RFC 9112 §6). `unsupported` : un
|
|
60
73
|
* codage de transfert autre que `chunked` seul — le proxy ne sait pas le
|
|
@@ -118,8 +131,9 @@ export declare function upstreamPath(url: string, mount: IProxyMount): string;
|
|
|
118
131
|
* Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
|
|
119
132
|
*
|
|
120
133
|
* 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.
|
|
122
|
-
*
|
|
134
|
+
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Sur
|
|
135
|
+
* `OPTIONS` et `TRACE`, `Max-Forwards` est décrémenté ; à `0`, le proxy répond
|
|
136
|
+
* 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
137
|
* connexion non établie à temps ou un amont muet rendent 504 — à condition
|
|
124
138
|
* que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
|
|
125
139
|
* statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
|
|
@@ -154,6 +168,20 @@ export declare function rawHead(status: string, rawHeaders: readonly string[], s
|
|
|
154
168
|
* @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`…)
|
|
155
169
|
*/
|
|
156
170
|
export declare function refuseUpgrade(socket: Duplex, status: number, reason: string, headers?: Readonly<Record<string, string | number>> | null): void;
|
|
171
|
+
/**
|
|
172
|
+
* Refuse un handshake WebSocket non conforme — la réponse qui convient à la
|
|
173
|
+
* raison rendue par `websocketHandshakeProblem`, seule implémentation pour le
|
|
174
|
+
* serveur WebSocket ET le proxy inverse.
|
|
175
|
+
*
|
|
176
|
+
* Une version inconnue reçoit `426` avec `Sec-WebSocket-Version: 13` : le
|
|
177
|
+
* client apprend vers quelle version se rabattre (RFC 6455 §4.2.2 point 6,
|
|
178
|
+
* §4.4), et le 426 porte l'`Upgrade` qu'exige RFC 9110 §15.5.22. Tout autre
|
|
179
|
+
* défaut de forme reçoit `400`.
|
|
180
|
+
*
|
|
181
|
+
* @param socket - socket du client, encore en attente de sa réponse
|
|
182
|
+
* @param problem - la raison rendue par `websocketHandshakeProblem`
|
|
183
|
+
*/
|
|
184
|
+
export declare function refuseMalformedHandshake(socket: Duplex, problem: string): void;
|
|
157
185
|
/**
|
|
158
186
|
* Relaie un upgrade WebSocket DÉJÀ validé (méthode, `Upgrade`, clé, version,
|
|
159
187
|
* `Origin` — cf `ReverseProxy.handleUpgrade`) : rejoue le handshake vers
|
|
@@ -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é
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type IncomingMessage } from "node:http";
|
|
2
2
|
import type { Duplex } from "node:stream";
|
|
3
|
-
import type
|
|
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.
|