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

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 (42) hide show
  1. package/dist/index.js +3 -2
  2. package/dist/nodefony/config/config.js +2 -1
  3. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  4. package/dist/nodefony/service/ProfilerAdminApi.js +12 -1
  5. package/dist/nodefony/service/audit-logger.js +3 -17
  6. package/dist/nodefony/service/http-kernel.js +80 -7
  7. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  8. package/dist/nodefony/src/context/Context.js +56 -13
  9. package/dist/nodefony/src/context/http/HttpContext.js +21 -0
  10. package/dist/nodefony/src/context/http2/Response.js +20 -1
  11. package/dist/nodefony/src/context/requestTrace.js +104 -0
  12. package/dist/nodefony/src/context/timingMode.js +21 -0
  13. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  14. package/dist/nodefony/src/context/websocket/WebsocketContext.js +12 -8
  15. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  16. package/dist/nodefony/src/profiler/Profiler.js +11 -21
  17. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  18. package/dist/types/index.d.ts +2 -0
  19. package/dist/types/nodefony/config/config.d.ts +4 -0
  20. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  21. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  22. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  23. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  24. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  25. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
  26. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  27. package/dist/types/nodefony/service/http-kernel.d.ts +50 -0
  28. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  29. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +15 -1
  30. package/dist/types/nodefony/src/context/http2/Response.d.ts +12 -0
  31. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  32. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  33. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  34. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +1 -1
  35. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  36. package/dist/types/nodefony/src/profiler/Profiler.d.ts +16 -101
  37. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  38. package/docs/diagnostics-channel.md +303 -0
  39. package/docs/index.md +6 -1
  40. package/docs/observabilite.md +94 -16
  41. package/docs/reverse-proxy.md +18 -16
  42. package/package.json +3 -3
@@ -134,6 +134,8 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
134
134
  private errorRenderer;
135
135
  private requestLogger;
136
136
  private profiler;
137
+ /** `timing.tooling` : profiler aussi l'outillage (lu une fois, à onReady). */
138
+ private profileTooling;
137
139
  private _trustProxyChecker;
138
140
  private _wsOriginPolicy;
139
141
  private rateLimiter;
@@ -186,6 +188,32 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
186
188
  * tel quel plutôt que d'inventer un état.
187
189
  */
188
190
  get rateLimitStore(): IRateLimitStore | null;
191
+ /**
192
+ * Cette requête — ou cette frame du pont — se profile-t-elle ? Jamais sans
193
+ * profileur (production). L'outillage (console, profileur, journaux, Vite :
194
+ * `isProfiledUrl`, sur la règle `isConsolePath` du cœur) seulement sous
195
+ * `timing.tooling` — on ne
196
+ * mesure pas la façon dont on mesure : ni tampon SQL, ni instantané, ni place
197
+ * dans les anneaux du profileur.
198
+ *
199
+ * @param url - URL ou chemin de la requête
200
+ * @returns `true` quand le profil doit être pris
201
+ */
202
+ shouldProfile(url: string): boolean;
203
+ /**
204
+ * En développement, signale une action qui a rendu une valeur que le
205
+ * Resolver ne sait pas envoyer (`null`, `undefined`, instance de classe)
206
+ * et n'a rien envoyé elle-même : la requête attend son délai d'expiration.
207
+ * Le message nomme l'action, ce qu'elle a rendu et le geste qui convient
208
+ * (`unsentResponseWarning`).
209
+ *
210
+ * Appelé au timeout, AVANT que le 504/408 parte — après, `sended` est vrai et
211
+ * plus rien ne se signalerait — et au démontage, pour un client parti avant.
212
+ * Gratuit en production : rien n'est lu au-delà de `waitAsync`.
213
+ *
214
+ * @param context - le contexte HTTP de la requête
215
+ */
216
+ warnUnsentResponse(context: HttpContext): void;
189
217
  private configureRateLimit;
190
218
  /**
191
219
  * (Re)configure le backstop OPT-IN de connexions WS concurrentes par IP (F6c)
@@ -474,6 +502,18 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
474
502
  * @returns le contexte servi, ou sa promesse.
475
503
  */
476
504
  handleHttp(scope: Scope, request: httpRequest, response: httpResponse, type: ServerType): MaybePromise<HttpContext>;
505
+ /**
506
+ * Sert un contexte HTTP fraîchement créé : le crochet `onCreateContext`
507
+ * s'il a des écouteurs, puis le pipeline.
508
+ *
509
+ * Hot path : `fireAsync` est une fonction async → 1 Promise + 1 microtask
510
+ * MÊME à 0 listener. En prod sans @nodefony/security ces seams
511
+ * (onCreateContext/beforeResolve/afterAuth) n'ont aucun listener → garde
512
+ * `listenerCount` (O(1), 0 alloc) pour ne RIEN planifier.
513
+ *
514
+ * @returns le contexte servi, ou sa promesse
515
+ */
516
+ private dispatchHttpContext;
477
517
  /**
478
518
  * Traceparent, tampon du profileur, puis le pipeline dans la bulle ALS de la
479
519
  * requête (`RequestContext.run`).
@@ -578,6 +618,16 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
578
618
  * @returns ce que rend l'action de handshake
579
619
  */
580
620
  handleWebsocket(scope: Scope, req: IncomingMessage, admission: IUpgradeAdmission, type: ServerType): Promise<unknown>;
621
+ /**
622
+ * Fin du canal `nodefony.http.request` pour un handshake interrompu par une
623
+ * exception — sans effet si un refus l'a déjà publiée (`refuseHandshake`).
624
+ * Le statut est un fait, pas une déduction : une socket rattachée dit que
625
+ * le `101` est parti ; sinon c'est le `500` du filet qui a été rendu.
626
+ *
627
+ * @param context - le contexte du handshake, `null` s'il n'a pu être construit
628
+ * @param error - l'exception qui a interrompu le pipeline
629
+ */
630
+ private closeFailedWebsocketTrace;
581
631
  private releaseOrphanWsScope;
582
632
  /**
583
633
  * Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
@@ -97,6 +97,12 @@ declare class Context extends Service implements IContextInterface {
97
97
  crossDomain: boolean;
98
98
  router: IRequestRouter | null;
99
99
  resolver: IRouteResolver | null;
100
+ /**
101
+ * Module qui signe les lignes de CETTE requête : la ligne d'accès et les
102
+ * lignes du contrôleur. `null` = le module de la route (sans route : le
103
+ * journal du noyau). Un contrôleur le change : `this.context.logModule = "billing"`.
104
+ */
105
+ logModule: string | null;
100
106
  /**
101
107
  * Intent de session de la route courante (posé par le Resolver depuis
102
108
  * `@UseSession` / paramètre `@Session`). Pilote le point d'activation unique
@@ -128,6 +134,9 @@ declare class Context extends Service implements IContextInterface {
128
134
  securityTrace: ISecurityTrace | null;
129
135
  private readonly _timingEnabled;
130
136
  private readonly _timingVerbose;
137
+ private readonly _clockEnabled;
138
+ private readonly _clockStartMs;
139
+ private _durationMs;
131
140
  readonly phases: PhaseTiming[];
132
141
  private _phaseIndex;
133
142
  private _afterResponseFns;
@@ -161,6 +170,21 @@ declare class Context extends Service implements IContextInterface {
161
170
  get timingEnabled(): boolean;
162
171
  phaseStart(name: PhaseName): void;
163
172
  phaseEnd(name: PhaseName): void;
173
+ /**
174
+ * Arrête l'horloge de la requête et rend sa durée, en millisecondes.
175
+ *
176
+ * La durée court de la construction du contexte au PREMIER appel : le
177
+ * démontage HTTP (`close` de la réponse, dernier octet remis — la mesure
178
+ * d'OpenTelemetry et du `$request_time` de nginx), la ligne d'accès pour un
179
+ * handshake WebSocket. Les appels suivants rendent la même valeur : journal
180
+ * d'accès, profiler et trace verbose lisent UNE durée, jamais trois.
181
+ *
182
+ * Active par défaut dans tous les environnements, production comprise :
183
+ * seul `timing.enabled: false` la coupe (cf `resolveTimingMode`).
184
+ *
185
+ * @returns la durée, ou `null` si `timing.enabled` vaut `false`
186
+ */
187
+ stopClock(): number | null;
164
188
  onAfterResponse(fn: AfterResponseHandler): void;
165
189
  /**
166
190
  * Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
@@ -176,7 +200,7 @@ declare class Context extends Service implements IContextInterface {
176
200
  private runAfterResponseHooks;
177
201
  get signal(): AbortSignal;
178
202
  _abortIfPending(reason?: string): void;
179
- log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
203
+ log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message, moduleName?: string): Pdu;
180
204
  /**
181
205
  * Confie au contexte le magasin `RequestContext` de son unité de travail
182
206
  * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
@@ -1,6 +1,7 @@
1
1
  import { ServerType, SchemeType } from "../../../service/http-kernel.js";
2
+ import HttpError from "../../errors/httpError.js";
2
3
  import Context, { HTTPMethod } from "../Context.js";
3
- import { Container, Scope } from "nodefony";
4
+ import { Container, Scope, nodefonyError } from "nodefony";
4
5
  import type { MaybePromise } from "nodefony";
5
6
  import HttpRequest from "./Request.js";
6
7
  import HttpResponse from "./Response.js";
@@ -50,6 +51,11 @@ declare class HttpContext extends Context implements IHttpContextInterface {
50
51
  private activeSocket;
51
52
  isRedirect: boolean;
52
53
  sended: boolean;
54
+ /**
55
+ * Requête relayée par le proxy inverse : la politique de ligne d'accès de
56
+ * son montage. `null` = servie par l'application (cas de toute requête).
57
+ */
58
+ relayAccessLog: "all" | "errors" | null;
53
59
  request: HttpRequestType;
54
60
  response: HttpRsponseType;
55
61
  uploadService: uploadService | null;
@@ -84,6 +90,14 @@ declare class HttpContext extends Context implements IHttpContextInterface {
84
90
  */
85
91
  handle(): MaybePromise<this>;
86
92
  setTimeout(): void;
93
+ /**
94
+ * Ligne d'accès — pour une requête RELAYÉE, avec le statut que l'amont a
95
+ * réellement rendu (le relais écrit la réponse brute, pas celle du
96
+ * contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
97
+ *
98
+ * @param httpError - l'erreur de la requête, s'il y en a une
99
+ */
100
+ logRequest(httpError?: Error | HttpError | nodefonyError | null): import("nodefony").Pdu | undefined;
87
101
  /**
88
102
  * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
89
103
  * persistante survit à la requête, et l'entrée de `socketActiveContext`
@@ -51,6 +51,18 @@ declare class Http2Response extends HttpResponse {
51
51
  * @throws de façon SYNCHRONE ce que lève la fin du flux.
52
52
  */
53
53
  end(chunk?: string | Buffer, encoding?: BufferEncoding): MaybePromise<http.ServerResponse | http2.ServerHttp2Stream>;
54
+ /**
55
+ * Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
56
+ * lui-même au `respond()`.
57
+ *
58
+ * Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
59
+ * démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
60
+ * hooks d'après-réponse, libération du scope DI), n'a jamais lieu. Les statuts
61
+ * que Node termine aussi d'office (204, 205, 304) émettent leur `close` seuls,
62
+ * et un flux détruit (client parti) aussi : ils ne sont pas touchés. Hors du
63
+ * chemin servi — appelé seulement quand le flux est déjà clos.
64
+ */
65
+ private settleHeadResponse;
54
66
  getStatusMessage(code?: number | string): string;
55
67
  getStatus(): {
56
68
  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;
@@ -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;
@@ -87,7 +87,7 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
87
87
  query: Record<string, string>;
88
88
  wsPath: string;
89
89
  constructor(scope: Scope, req: IncomingMessage, ws: Ws | null, serverType: ServerType);
90
- log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): import("nodefony").Pdu;
90
+ log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message, moduleName?: string): import("nodefony").Pdu;
91
91
  logRequest(httpError?: Error | HttpError | nodefonyError | null, acceptedProtocol?: string | null): import("nodefony").Pdu | undefined;
92
92
  /**
93
93
  * Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
@@ -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
@@ -20,106 +20,12 @@
20
20
  */
21
21
  import type { ProfiledResolver } from "./FrameProfile.js";
22
22
  import type { ProtocolName } from "../../interfaces/IContext.js";
23
- /** Une phase du pipeline, projetée pour le transport (durée résolue). */
24
- export interface ProfilePhase {
25
- name: string;
26
- /** Début relatif `performance.now()` (ms) — sert au calcul du waterfall. */
27
- startMs: number;
28
- /** Durée en ms, `null` si la phase ne s'est pas terminée (erreur). */
29
- durationMs: number | null;
30
- }
31
- /**
32
- * Une requête SQL/NoSQL exécutée pendant la requête HTTP (SEAM ORM — futur).
33
- *
34
- * Non collecté aujourd'hui : quand `@nodefony/orm-core` arrivera, les adapters
35
- * pousseront leurs requêtes dans un buffer per-request dev-only (via l'ALS
36
- * `RequestContext.getRequestId()`, déjà en place) que `collect()` lira ici.
37
- * Le champ `queries` reste donc `undefined` tant qu'aucun ORM ne pushe → 0 coût.
38
- */
39
- export interface ProfileQuery {
40
- /** Requête (SQL ou commande NoSQL), tronquée si volumineuse. */
41
- sql: string;
42
- /** Début relatif (`performance.now()`) — même horloge que les phases. */
43
- startMs?: number | undefined;
44
- /** Durée d'exécution en ms. */
45
- durationMs: number;
46
- /** Lignes affectées/retournées, si connu. */
47
- rows?: number | undefined;
48
- /** Connecteur émetteur (`drizzle`, `mongoose`…). */
49
- connector?: string | undefined;
50
- }
51
- /**
52
- * Traversée du firewall par cette requête — **ce qui était possible** (la zone :
53
- * son nom, si elle est protégée, quels authenticators elle accepte) croisé avec
54
- * **ce qui s'est passé** (quel maillon a résolu l'identité, l'issue, le motif
55
- * d'un refus).
56
- *
57
- * Sans cela, une requête qui PASSE est invisible côté sécurité : le chemin de
58
- * succès n'émet aucun événement d'audit (choix délibéré — le volume nominal
59
- * n'est pas un signal). `undefined` hors zone firewall.
60
- */
61
- export interface ProfileSecurity {
62
- /** Nom de la zone traversée. */
63
- zone: string | null;
64
- /** La zone exige-t-elle une identité (`security: true`) ? */
65
- protected: boolean;
66
- /** Chaîne d'authenticators : `first` = le premier qui supporte, `all` = MFA. */
67
- mode: string | null;
68
- /** Authenticators que la zone accepte (ce qui était POSSIBLE). */
69
- candidates: string[];
70
- /** Authenticator qui a RÉELLEMENT résolu l'identité. */
71
- authenticator: string | null;
72
- /** `granted` · `anonymous` · `denied` · `failure` · `throttled` · `bypass` · `public`. */
73
- outcome: string | null;
74
- /** Motif du refus (`no_credentials`, `invalid_credentials`…). */
75
- reason: string | null;
76
- /** Rôles du token résolu (l'axe autorisation). */
77
- roles: string[] | null;
78
- }
79
- /** Profil complet d'une requête (HTTP ou message WS), exposé par `get`. */
80
- export interface ProfileEntry {
81
- requestId: string;
82
- /**
83
- * Numéro d'ordre de collecte, croissant sur la vie du profileur. Un profil
84
- * réinséré (identifiant réutilisé) reçoit un numéro neuf.
85
- */
86
- seq: number;
87
- /** Horodatage de la collecte (`Date.now()`), pour le tri client. */
88
- ts: number;
89
- kind: "http" | "ws";
90
- method: string | null;
91
- url: string;
92
- scheme: string;
93
- status: number | null;
94
- /** Durée totale serveur (ms), dérivée des phases. */
95
- durationMs: number | null;
96
- route: string | null;
97
- controller: string | null;
98
- action: string | null;
99
- remoteAddress: string | null;
100
- /** Identité (username) si le firewall l'a injectée, sinon `null`. */
101
- user: string | null;
102
- /** `traceparent` W3C Trace Context (P2.7) — corrélation distribuée RFC-propre. */
103
- traceparent: string | null;
104
- error: string | null;
105
- phases: ProfilePhase[];
106
- /** Requêtes ORM (SEAM futur — `undefined` tant qu'aucun adapter ne pushe). */
107
- queries?: ProfileQuery[] | undefined;
108
- /** Traversée du firewall — `undefined` si la requête n'a croisé aucune zone. */
109
- security?: ProfileSecurity | undefined;
110
- }
111
- /** Résumé léger d'un profil pour la liste `recent` (sans les phases). */
112
- export interface ProfileSummary {
113
- requestId: string;
114
- ts: number;
115
- kind: "http" | "ws";
116
- method: string | null;
117
- url: string;
118
- status: number | null;
119
- durationMs: number | null;
120
- route: string | null;
121
- error: string | null;
122
- }
23
+ import type { IProfileEntry, IProfilePhase, IProfileQuery, IProfileSecurity, IProfileSummary } from "nodefony";
24
+ export type ProfilePhase = IProfilePhase;
25
+ export type ProfileQuery = IProfileQuery;
26
+ export type ProfileSecurity = IProfileSecurity;
27
+ export type ProfileEntry = IProfileEntry;
28
+ export type ProfileSummary = IProfileSummary;
123
29
  /** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
124
30
  interface ProfilableContext {
125
31
  requestId?: string | undefined;
@@ -142,6 +48,11 @@ interface ProfilableContext {
142
48
  durationMs?: number | undefined;
143
49
  }> | undefined;
144
50
  resolver?: ProfiledResolver | null | undefined;
51
+ /**
52
+ * Arrête l'horloge de la requête et rend sa durée (ms) — la même que le
53
+ * journal d'accès (`IContext.stopClock`). `null` si `timing.enabled` vaut `false`.
54
+ */
55
+ stopClock(): number | null;
145
56
  /**
146
57
  * Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
147
58
  * que la payload ALS — les adapters ORM y poussent via `RequestContext`.
@@ -222,8 +133,12 @@ export declare class Profiler implements IProfilerReader {
222
133
  get(requestId: string): ProfileEntry | undefined;
223
134
  /**
224
135
  * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
136
+ *
137
+ * @param limit - nombre maximal de résumés
138
+ * @param connection - le requestId d'une connexion WebSocket : ses SEULES
139
+ * frames du pont (`<connection>.<n>`, règle `frameIndexOf` du cœur)
225
140
  */
226
- recent(limit?: number): ProfileSummary[];
141
+ recent(limit?: number, connection?: string): ProfileSummary[];
227
142
  /** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
228
143
  get cursor(): number;
229
144
  /**
@@ -1,4 +1,6 @@
1
1
  import type { ProfileEntry } from "./Profiler.js";
2
+ import type { IStatusBreakdown, IPhaseTotal, IRouteStats, INPlusOne, ISqlShapeStats, IErrorSample, IProfileReport } from "nodefony";
3
+ export type { IStatusBreakdown, IPhaseTotal, IRouteStats, INPlusOne, ISqlShapeStats, IErrorSample, IProfileReport, };
2
4
  /**
3
5
  * Agrégation des profils de requêtes — règle PURE, sans horloge ni I/O.
4
6
  *
@@ -21,7 +23,14 @@ export declare const ROUTE_SAMPLE_CAP = 256;
21
23
  export declare const SQL_SHAPE_CAP = 200;
22
24
  /** Exemples d'erreurs retenus (statut ≥ 500 ou exception). */
23
25
  export declare const ERROR_SAMPLE_CAP = 10;
24
- /** Préfixe des routes de l'outillage (console, API d'administration, porte MCP). */
26
+ /**
27
+ * Préfixe de l'espace réservé au framework.
28
+ *
29
+ * @deprecated Ne dit pas ce qui est de l'outillage : une connexion
30
+ * d'utilisateur (`/nodefony/security/api/auth/…`) ou une route d'application
31
+ * (`/nodefony/test/index`) passent sous ce préfixe. La règle est
32
+ * `isConsolePath` (`nodefony`), que `profileLane` appelle.
33
+ */
25
34
  export declare const TOOLING_URL_PREFIX = "/nodefony/";
26
35
  /**
27
36
  * Provenance d'un profil : l'application, l'outillage (`/nodefony/…` :
@@ -30,8 +39,10 @@ export declare const TOOLING_URL_PREFIX = "/nodefony/";
30
39
  */
31
40
  export type ProfileLane = "app" | "tooling" | "asset";
32
41
  /**
33
- * Range un profil par provenance, d'après son URL. Seule règle du partage —
34
- * les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
42
+ * Range un profil par provenance, d'après son URL. L'outillage est reconnu par
43
+ * la règle du cœur, `isConsolePath` — la même que les Journaux, la barre de
44
+ * débogage et le Studio : une connexion d'utilisateur reste de l'application.
45
+ * Les anneaux du profileur et le filtre de l'agrégation la lisent tous deux.
35
46
  *
36
47
  * @param url - URL du profil, absolue ou chemin.
37
48
  * @returns la provenance du profil.
@@ -44,113 +55,19 @@ export declare function profileLane(url: string): ProfileLane;
44
55
  * @param url - URL du profil, absolue ou chemin.
45
56
  */
46
57
  export declare function isToolingUrl(url: string): boolean;
58
+ /**
59
+ * Une requête de cette URL se profile-t-elle ? L'application toujours ;
60
+ * l'outillage et les assets relayés seulement sur demande (`timing.tooling`)
61
+ * — on ne mesure pas la façon dont on mesure.
62
+ *
63
+ * @param url - URL ou chemin de la requête (ou de la frame du pont)
64
+ * @param tooling - profiler aussi l'outillage
65
+ */
66
+ export declare function isProfiledUrl(url: string, tooling: boolean): boolean;
47
67
  /** Groupe des requêtes qu'aucune route n'a servies (404, statique, sondes). */
48
68
  export declare const NO_ROUTE = "(aucune route)";
49
69
  /** Pseudo-phase : le temps de l'action hors phases imbriquées et hors SQL. */
50
70
  export declare const ACTION_SELF_PHASE = "action.self";
51
- /** Répartition des statuts HTTP d'une route. */
52
- export interface IStatusBreakdown {
53
- s2xx: number;
54
- s3xx: number;
55
- s4xx: number;
56
- s5xx: number;
57
- /** Dont 401 : la route exige une authentification que la requête n'avait pas. */
58
- unauthorized: number;
59
- /** Dont 403 : authentifiée, mais refusée. */
60
- forbidden: number;
61
- }
62
- /** Temps cumulé d'une phase sur les requêtes d'une route. */
63
- export interface IPhaseTotal {
64
- totalMs: number;
65
- /** Requêtes où la phase est apparue et s'est terminée. */
66
- count: number;
67
- }
68
- /** Statistiques d'une route sur la fenêtre agrégée. */
69
- export interface IRouteStats {
70
- route: string;
71
- kind: "http" | "ws";
72
- controller: string | null;
73
- action: string | null;
74
- count: number;
75
- /** Requêtes sans durée mesurée (mesure des phases désactivée, erreur précoce). */
76
- untimed: number;
77
- /**
78
- * Requêtes SERVIES et chronométrées (statut < 400, sans exception) : la
79
- * population de tout ce qui se dit « par requête » ci-dessous. Refus et
80
- * erreurs se lisent dans `status` et `errors`.
81
- */
82
- served: number;
83
- /** Percentiles et maximum, sur les requêtes servies. */
84
- p50Ms: number | null;
85
- p95Ms: number | null;
86
- maxMs: number | null;
87
- status: IStatusBreakdown;
88
- /** Requêtes en erreur : statut ≥ 500 ou exception levée. */
89
- errors: number;
90
- /**
91
- * Requêtes SQL/NoSQL des requêtes servies : total, maximum dans une
92
- * requête, temps CUMULÉ (des requêtes parallèles s'y additionnent).
93
- */
94
- queries: {
95
- total: number;
96
- perRequestMax: number;
97
- totalMs: number;
98
- };
99
- /** Temps par phase des requêtes servies, `action.self` compris. */
100
- phases: Record<string, IPhaseTotal>;
101
- }
102
- /** Un N+1 : la même forme SQL répétée dans une requête. */
103
- export interface INPlusOne {
104
- route: string;
105
- controller: string | null;
106
- action: string | null;
107
- shape: string;
108
- /** Plus grand nombre de répétitions vu dans une requête. */
109
- maxRepeats: number;
110
- /** Requêtes touchées. */
111
- requests: number;
112
- /** Temps cumulé de ces répétitions. */
113
- totalMs: number;
114
- /** Une requête touchée, pour aller voir son profil complet. */
115
- sampleRequestId: string;
116
- }
117
- /** Une forme SQL et son coût sur la fenêtre. */
118
- export interface ISqlShapeStats {
119
- shape: string;
120
- connector: string | null;
121
- count: number;
122
- totalMs: number;
123
- maxMs: number;
124
- /** Routes qui l'ont émise (au plus cinq). */
125
- routes: string[];
126
- }
127
- /** Un exemple d'erreur, de quoi la retrouver dans le journal. */
128
- export interface IErrorSample {
129
- route: string;
130
- status: number | null;
131
- error: string | null;
132
- requestId: string;
133
- }
134
- /** Le rapport agrégé. */
135
- export interface IProfileReport {
136
- /** Requêtes agrégées (hors outillage filtré). */
137
- requests: number;
138
- http: number;
139
- ws: number;
140
- /** Requêtes de l'outillage écartées (`/nodefony/…`, `/_vite/…`). */
141
- filteredTooling: number;
142
- /** Requêtes sans durée mesurée. */
143
- untimed: number;
144
- /** Triées par p95 décroissant. */
145
- routes: IRouteStats[];
146
- /** Triés par temps cumulé décroissant. */
147
- nPlusOne: INPlusOne[];
148
- /** Les dix formes SQL les plus coûteuses (temps cumulé décroissant). */
149
- slowQueries: ISqlShapeStats[];
150
- /** Formes SQL distinctes vues au-delà de la capacité, comptées sans détail. */
151
- sqlShapesDropped: number;
152
- errors: IErrorSample[];
153
- }
154
71
  /** Réglages d'une agrégation. */
155
72
  export interface IProfileAggregatorOptions {
156
73
  /** Répétitions à partir desquelles une forme SQL est un N+1. */