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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -25
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +6 -3
  7. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  8. package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
  9. package/dist/nodefony/service/audit-logger.js +3 -17
  10. package/dist/nodefony/service/error-renderer.js +6 -4
  11. package/dist/nodefony/service/http-kernel.js +191 -40
  12. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  13. package/dist/nodefony/service/request-logger.js +1 -1
  14. package/dist/nodefony/service/reverse-proxy.js +7 -2
  15. package/dist/nodefony/service/servers/clientError.js +28 -4
  16. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  17. package/dist/nodefony/service/servers/server-http.js +5 -3
  18. package/dist/nodefony/service/servers/server-https.js +12 -5
  19. package/dist/nodefony/service/servers/server-static.js +1 -6
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  21. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  22. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  23. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  24. package/dist/nodefony/service/upload/upload-service.js +2 -2
  25. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  26. package/dist/nodefony/src/context/Context.js +56 -13
  27. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  28. package/dist/nodefony/src/context/http/HttpContext.js +75 -25
  29. package/dist/nodefony/src/context/http/Request.js +15 -6
  30. package/dist/nodefony/src/context/http/Response.js +354 -35
  31. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  32. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  34. package/dist/nodefony/src/context/http2/Request.js +1 -1
  35. package/dist/nodefony/src/context/http2/Response.js +28 -5
  36. package/dist/nodefony/src/context/requestTrace.js +104 -0
  37. package/dist/nodefony/src/context/timingMode.js +21 -0
  38. package/dist/nodefony/src/context/transport.js +20 -1
  39. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  40. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
  42. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  43. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  44. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  45. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  46. package/dist/nodefony/src/cookies/cookie.js +54 -13
  47. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  48. package/dist/nodefony/src/profiler/Profiler.js +25 -22
  49. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  50. package/dist/nodefony/src/proxy/forward.js +109 -9
  51. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  52. package/dist/nodefony/src/proxy/rules.js +29 -14
  53. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  54. package/dist/types/index.d.ts +8 -0
  55. package/dist/types/nodefony/config/config.d.ts +8 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  57. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  58. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  61. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  62. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  64. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  65. package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
  66. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  68. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  69. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  70. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  71. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
  72. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  73. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  74. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  75. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  76. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  79. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  80. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  81. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
  83. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  84. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  85. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  86. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  87. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  88. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  89. package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
  90. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  91. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  92. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  93. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  94. package/docs/byte-stream.md +371 -0
  95. package/docs/cookies.md +13 -3
  96. package/docs/diagnostics-channel.md +303 -0
  97. package/docs/index.md +14 -1
  98. package/docs/observabilite.md +94 -16
  99. package/docs/reverse-proxy.md +33 -19
  100. package/docs/servers.md +26 -15
  101. package/docs/session.md +3 -3
  102. package/docs/sse.md +27 -20
  103. package/package.json +16 -16
@@ -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 posés sur la réponse, puis
13
- * ceux passés à `writeHead`, qui gagnent.
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,6 +53,18 @@ declare class Http2Response extends HttpResponse {
51
53
  * @throws de façon SYNCHRONE ce que lève la fin du flux.
52
54
  */
53
55
  end(chunk?: string | Buffer, encoding?: BufferEncoding): MaybePromise<http.ServerResponse | http2.ServerHttp2Stream>;
56
+ /**
57
+ * Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
58
+ * lui-même au `respond()`.
59
+ *
60
+ * Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
61
+ * démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
62
+ * hooks d'après-réponse, libération du scope DI), n'a jamais lieu. Les statuts
63
+ * que Node termine aussi d'office (204, 205, 304) émettent leur `close` seuls,
64
+ * et un flux détruit (client parti) aussi : ils ne sont pas touchés. Hors du
65
+ * chemin servi — appelé seulement quand le flux est déjà clos.
66
+ */
67
+ private settleHeadResponse;
54
68
  getStatusMessage(code?: number | string): string;
55
69
  getStatus(): {
56
70
  code: number;
@@ -0,0 +1,64 @@
1
+ import type { IRequestTrace, RequestTraceProtocol } from "../../interfaces/IRequestTrace.js";
2
+ import type Context from "./Context.js";
3
+ /**
4
+ * Nom du `TracingChannel` qui encadre chaque requête HTTP et chaque handshake
5
+ * WebSocket — forme `module.fonction` recommandée par la documentation de
6
+ * `node:diagnostics_channel` ; les sous-canaux sont
7
+ * `tracing:nodefony.http.request:start`, `:end`, `:asyncStart`, `:asyncEnd`,
8
+ * `:error`.
9
+ */
10
+ export declare const REQUEST_TRACE_CHANNEL = "nodefony.http.request";
11
+ /**
12
+ * Le message tel que Nodefony le remplit au fil de la requête : la vue
13
+ * modifiable d'{@link IRequestTrace}, qui reste en lecture seule pour
14
+ * l'abonné.
15
+ */
16
+ export type RequestTraceRecord = {
17
+ -readonly [K in keyof IRequestTrace]: IRequestTrace[K];
18
+ };
19
+ /**
20
+ * Dit si quelqu'un écoute le canal — LA garde du chemin chaud : ~1,6 ns
21
+ * mesurées, aucune allocation. Lue une seule fois, au début de la requête :
22
+ * un abonné arrivé ensuite ne voit que les requêtes suivantes (règle des
23
+ * `TracingChannel`, qui interdit une fin sans début).
24
+ *
25
+ * @returns `true` si au moins un sous-canal a un abonné
26
+ */
27
+ export declare function hasRequestTraceSubscribers(): boolean;
28
+ /**
29
+ * Crée le message de la requête et le range sur son contexte. À n'appeler
30
+ * que sous {@link hasRequestTraceSubscribers} : c'est la seule allocation
31
+ * du canal, et elle n'a lieu que si quelqu'un écoute.
32
+ *
33
+ * @param context - le contexte qui vient d'être construit
34
+ * @param protocol - requête HTTP ou handshake WebSocket
35
+ * @returns le message, de forme fixe, dont les champs de résultat valent `null`
36
+ */
37
+ export declare function openRequestTrace(context: Context, protocol: RequestTraceProtocol): RequestTraceRecord;
38
+ /**
39
+ * Exécute la partie synchrone du traitement sous `start.runStores` puis
40
+ * publie `end` — la séquence de `TracingChannel.tracePromise`. Les stores
41
+ * liés au canal (`bindStore`, ce que fait un agent APM) restent actifs pour
42
+ * toute la suite asynchrone de la requête.
43
+ *
44
+ * @param trace - le message de la requête
45
+ * @param fn - la fonction qui sert la requête
46
+ * @param thisArg - le receveur de `fn`
47
+ * @param args - les arguments de `fn`
48
+ * @returns ce que rend `fn` — un contexte ou sa promesse
49
+ */
50
+ export declare function runTraced<ThisArg, Args extends unknown[], Result>(trace: RequestTraceRecord, fn: (this: ThisArg, ...args: Args) => Result, thisArg: ThisArg, ...args: Args): Result;
51
+ /**
52
+ * Termine la trace de la requête : remplit les champs de résultat puis
53
+ * publie `error` (s'il y a une exception), `asyncStart` et `asyncEnd`.
54
+ * Idempotent — le premier appel gagne, les suivants ne font rien : un
55
+ * handshake WebSocket a plusieurs sorties, une seule publie la fin.
56
+ *
57
+ * La durée est celle de l'horloge unique du contexte (`stopClock`), la même
58
+ * que celle de la ligne d'accès : le canal n'en mesure aucune.
59
+ *
60
+ * @param context - le contexte de la requête
61
+ * @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
62
+ * @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
63
+ */
64
+ export declare function closeRequestTrace(context: Context, statusCode: number | null, error: unknown): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Ce que le chronométrage mesure sur une requête :
3
+ *
4
+ * - `"off"` — rien : ni horloge, ni phases ; la ligne d'accès sort sans durée ;
5
+ * - `"clock"` — la durée seule : une lecture d'horloge à l'entrée, une à la fin ;
6
+ * - `"phases"` — la durée ET le détail par phase (resolve, firewall, action…).
7
+ */
8
+ export type TimingMode = "off" | "clock" | "phases";
9
+ /**
10
+ * Résout le mode de chronométrage d'une requête depuis `timing.enabled` et
11
+ * l'environnement — la SEULE règle, appelée par le constructeur du `Context`.
12
+ *
13
+ * Défaut (`null`, pas d'avis) : la durée partout, les phases hors production.
14
+ * La durée coûte deux lectures d'horloge, l'équivalent de quelques pourcents
15
+ * de la ligne d'accès qu'elle complète ; les phases coûtent une lecture par
16
+ * phase, un tableau et un index par requête — réservées au développement, ou
17
+ * à un diagnostic à chaud (`true`). `false` coupe tout, durée comprise.
18
+ *
19
+ * @param enabled - valeur de `timing.enabled` (`true`, `false`, `null`)
20
+ * @param environment - environnement normalisé du noyau (`"production"`…)
21
+ * @returns le mode de la requête — une chaîne littérale, rien d'alloué
22
+ */
23
+ export declare function resolveTimingMode(enabled: unknown, environment: string | undefined): TimingMode;
@@ -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;
@@ -0,0 +1,16 @@
1
+ import type { IRouteResolver } from "../../interfaces/IRouting.js";
2
+ /**
3
+ * Rédige l'avertissement de développement d'une requête restée sans réponse :
4
+ * l'action a rendu une valeur que le framework ne sait pas envoyer, et la
5
+ * requête a attendu son délai d'expiration.
6
+ *
7
+ * Le message nomme l'action fautive, ce qu'elle a rendu, et le geste qui
8
+ * convient à ce cas — `null` est presque toujours une entité absente, qui
9
+ * appelle un 404.
10
+ *
11
+ * @param resolver - le resolver de la requête (route, controller, action,
12
+ * valeur non rendue), `null` si aucune route n'a été résolue.
13
+ * @param url - l'URL de la requête, repli quand aucune route n'est connue.
14
+ * @returns le texte de l'avertissement.
15
+ */
16
+ export declare function unsentResponseWarning(resolver: IRouteResolver | null | undefined, url: string): string;
@@ -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
@@ -87,7 +101,7 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
87
101
  query: Record<string, string>;
88
102
  wsPath: string;
89
103
  constructor(scope: Scope, req: IncomingMessage, ws: Ws | null, serverType: ServerType);
90
- log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): import("nodefony").Pdu;
104
+ log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message, moduleName?: string): import("nodefony").Pdu;
91
105
  logRequest(httpError?: Error | HttpError | nodefonyError | null, acceptedProtocol?: string | null): import("nodefony").Pdu | undefined;
92
106
  /**
93
107
  * Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
@@ -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 = "drop" | "close";
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 = "send" | "drop" | "close";
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, 0 dépendance) du CONTENU d'un message WebSocket pour
3
- * le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
4
- * être testable aux limites sans serveur.
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 sérialisée
10
- * (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme et
11
- * faux). Seules string et objets « JSON » sont rendus en texte (borné).
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 dump) ;
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, parser };
106
+ export { cookiesParser };
@@ -80,6 +80,12 @@ export declare class FrameProfile {
80
80
  * invocation, il redevient exact, et le SQL se replace dans le waterfall.
81
81
  */
82
82
  readonly profilerQueries: IProfilerQuery[] | null;
83
+ /**
84
+ * Ce profil sera-t-il gardé par le Profiler ? Faux quand seul le timing
85
+ * l'a ouvert (chemin d'outillage) : son id ne désigne alors aucun profil
86
+ * lisible, et ne doit pas être annoncé au client.
87
+ */
88
+ get profiled(): boolean;
83
89
  /** Resolver de la frame — porte route / controller / action. */
84
90
  resolver: ProfiledResolver | null;
85
91
  /** Statut HTTP-équivalent de l'invocation (200, 403, 404…). */
@@ -90,7 +96,16 @@ export declare class FrameProfile {
90
96
  message: string;
91
97
  } | null;
92
98
  private readonly timing;
99
+ private readonly clockStartMs;
100
+ private durationMs;
93
101
  constructor(init: FrameProfileInit);
102
+ /**
103
+ * Arrête l'horloge de la frame (au premier appel) et rend sa durée en ms ;
104
+ * `null` si le timing est éteint. Contrat de `IContext.stopClock`.
105
+ *
106
+ * @returns la durée de l'invocation, ou `null`
107
+ */
108
+ stopClock(): number | null;
94
109
  /**
95
110
  * Ouvre une phase. Contrairement au `Context`, l'index est un **scan arrière**
96
111
  * (pas de `Map<nom, index>`) : une frame porte une poignée de phases, et une