@nodefony/http 10.0.0-alpha.1

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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,37 @@
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.
5
+ *
6
+ * 🔒 Robustesse binaire (cf doc `ws` — `socket.send(data)` accepte
7
+ * `String | Number | Object | Buffer | ArrayBuffer | TypedArray | DataView |
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é).
12
+ */
13
+ /** Cap de troncature du contenu loggé (octets/chars). Borne ring + JSONL. */
14
+ export declare const WS_LOG_CONTENT_CAP = 4096;
15
+ /**
16
+ * Taille en octets d'une charge **binaire** reconnue, ou `-1` si la valeur n'est
17
+ * pas binaire (→ à traiter en JSON/texte). Couvre tous les types binaires que
18
+ * `ws` accepte/livre : `Buffer`, `ArrayBuffer`, vues (`TypedArray`/`DataView`),
19
+ * `Blob` (Node ≥ 18), et `Buffer[]` (fragments — binaire ssi TOUS ses éléments
20
+ * le sont, sinon c'est un tableau JSON ordinaire).
21
+ *
22
+ * @param data - valeur à mesurer.
23
+ * @returns nombre d'octets, ou `-1` si non binaire.
24
+ */
25
+ export declare function binaryByteLength(data: unknown): number;
26
+ /**
27
+ * Formate une charge utile WS en chaîne **bornée** et **sûre** pour le log :
28
+ * - `string` → tronquée à `cap` (+ ellipse) ;
29
+ * - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de dump) ;
30
+ * - `null`/`undefined` → `""` ;
31
+ * - objet « JSON » → `JSON.stringify` compact tronqué (cycle, `bigint`, valeur
32
+ * non sérialisable → repli `String(...)`).
33
+ *
34
+ * @param data - charge utile (RECEIVE/SEND/BROADCAST).
35
+ * @param cap - longueur max avant troncature (défaut {@link WS_LOG_CONTENT_CAP}).
36
+ */
37
+ export declare function formatWsLogContent(data: unknown, cap?: number): string;
@@ -0,0 +1,88 @@
1
+ import { StringValue } from "ms";
2
+ import { ContextType } from "../../service/http-kernel.js";
3
+ import type { ICookie as ICookieInterface, ICookieOptions, IWsCookie, PriorityType, SameSiteType } from "../../interfaces/ICookie.js";
4
+ declare module "http" {
5
+ interface IncomingMessage {
6
+ cookies: Record<string, Cookie>;
7
+ }
8
+ }
9
+ declare module "http2" {
10
+ interface Http2ServerRequest {
11
+ cookies: Record<string, Cookie>;
12
+ }
13
+ }
14
+ /**
15
+ * Options d'un cookie — alias du contrat public {@link ICookieOptions}.
16
+ *
17
+ * Conservé sous ce nom parce que `session.ts` et l'historique du module
18
+ * l'importent ainsi ; ce n'est PAS une seconde définition. Un consommateur
19
+ * externe nomme le type par `ICookieOptions`, exporté au barrel.
20
+ */
21
+ export type CookieOptionsType = ICookieOptions;
22
+ declare function parser(strToParse: string): import("cookie").Cookies;
23
+ declare function cookiesParser(context: ContextType): void;
24
+ declare class Cookie implements ICookieInterface {
25
+ options: CookieOptionsType;
26
+ name: string;
27
+ signed?: boolean;
28
+ value: unknown;
29
+ originalMaxAge?: number;
30
+ expires?: Date;
31
+ maxAge?: number;
32
+ path?: string;
33
+ domain: string | undefined;
34
+ httpOnly?: boolean;
35
+ secure?: boolean;
36
+ sameSite?: SameSiteType;
37
+ priority?: string;
38
+ constructor(cookies: Cookie);
39
+ constructor(name: string, value: unknown, options?: CookieOptionsType);
40
+ clearCookie(): void;
41
+ setValue(value: unknown): unknown;
42
+ setSecure(val: boolean | undefined): boolean;
43
+ setDomain(): string | undefined;
44
+ setHttpOnly(val: boolean | undefined): boolean;
45
+ setPath(val: string | undefined): string | undefined;
46
+ /**
47
+ * Normalise l'attribut `SameSite` vers une valeur canonique RFC 6265bis.
48
+ *
49
+ * Fallback **`Lax`** (jamais `None`) : `None` désactive la protection CSRF et
50
+ * impose `Secure` — ce n'est jamais un défaut sûr. Toute entrée inconnue
51
+ * (ancien `boolean`, casse libre) retombe sur `Lax` (fail-safe).
52
+ *
53
+ * @param val - valeur souhaitée (`Strict` / `Lax` / `None`, casse libre).
54
+ * @returns la forme canonique title-case.
55
+ */
56
+ setSameSite(val: SameSiteType | undefined): SameSiteType;
57
+ setExpires(date: Date | string | number | undefined): Date | undefined;
58
+ setOriginalMaxAge(ms: number | StringValue): number;
59
+ setPriority(val: PriorityType): PriorityType;
60
+ getMaxAge(): number | undefined;
61
+ toString(): string;
62
+ /**
63
+ * Signe une valeur de cookie : `HMAC-SHA256(value)` clé = `secret`, encodé en
64
+ * base64url (sûr en cookie, pas de `=`/`+`/`/`). Retourne `value.signature`
65
+ * → la valeur d'origine est PRÉSERVÉE (récupérable via {@link unsign}).
66
+ *
67
+ * @param val - valeur en clair à signer.
68
+ * @param secret - clé HMAC (le secret, JAMAIS la valeur).
69
+ * @returns `value.base64url(hmac)`.
70
+ * @throws {TypeError} si `val` n'est pas une string ou `secret` est vide.
71
+ */
72
+ sign(val: unknown, secret: string): string;
73
+ /**
74
+ * Vérifie et déballe une valeur signée par {@link sign} (tolère le préfixe
75
+ * `s:`). Comparaison **timing-safe** (`crypto.timingSafeEqual`).
76
+ *
77
+ * @param val - valeur signée (`value.signature`, éventuellement préfixée `s:`).
78
+ * Défaut : `this.value`.
79
+ * @param secret - clé HMAC. Défaut : `this.options.secret`.
80
+ * @returns la valeur en clair si la signature est valide, sinon `false`.
81
+ * @throws {TypeError} si les types sont invalides ou le secret est vide.
82
+ */
83
+ unsign(val?: string, secret?: string): string | false;
84
+ serialize(): string;
85
+ serializeWebSocket(): IWsCookie;
86
+ }
87
+ export default Cookie;
88
+ export { cookiesParser, parser };
@@ -0,0 +1,15 @@
1
+ import { nodefonyError as NodefonyError } from "nodefony";
2
+ import { ContextType } from "../../service/http-kernel.js";
3
+ import { HttpRequestType, HttpRsponseType } from "../context/http/HttpContext.js";
4
+ declare class HttpError extends NodefonyError {
5
+ context?: ContextType;
6
+ response?: HttpRsponseType;
7
+ request?: HttpRequestType;
8
+ url?: string;
9
+ controller?: string;
10
+ action?: string;
11
+ jsonResponse?: string;
12
+ constructor(message?: unknown, code?: number, context?: ContextType);
13
+ toString(): string;
14
+ }
15
+ export default HttpError;
@@ -0,0 +1,110 @@
1
+ import type { IProfilerQuery } from "nodefony";
2
+ import type { ISecurityTrace, PhaseName, PhaseTiming } from "../../interfaces/IContext.js";
3
+ /**
4
+ * Zone firewall capturée sur le contexte (`SecuredArea`, lue en structurel :
5
+ * `@nodefony/http` ne peut pas importer `@nodefony/security`).
6
+ */
7
+ export interface ProfiledArea {
8
+ name?: string;
9
+ security?: boolean;
10
+ mode?: string;
11
+ authenticators?: readonly string[];
12
+ }
13
+ /** Ce que le Profiler lit d'un Resolver (route/controller/action). */
14
+ export interface ProfiledResolver {
15
+ route?: {
16
+ name?: string;
17
+ } | null;
18
+ controller?: {
19
+ name?: string;
20
+ } | null;
21
+ actionName?: string;
22
+ }
23
+ /** Descripteur d'ouverture d'une invocation (ce que le pont connaît d'entrée). */
24
+ export interface FrameProfileInit {
25
+ /** Identifiant du profil — `<requestId de la connexion>.<n° de frame>`. */
26
+ requestId: string;
27
+ /** Type de transport du contexte porteur (`websocket` / `websocket-secure`). */
28
+ type: string;
29
+ scheme: string;
30
+ /** Méthode LOGIQUE de l'invocation (`GET`, `POST`…), pas le transport. */
31
+ method: string;
32
+ /** Chemin invoqué par la frame (avec sa query), pas l'URL de la connexion. */
33
+ url: string;
34
+ remoteAddress: string | null;
35
+ traceparent: string | null;
36
+ /** Zone firewall de la connexion (l'identité de la frame en découle). */
37
+ security: ProfiledArea | null;
38
+ /** Décision du firewall au handshake — l'identité que la frame rejoue. */
39
+ securityTrace: ISecurityTrace | null;
40
+ /** Mesurer les phases ? (`timing.enabled` du contexte porteur.) */
41
+ timing: boolean;
42
+ /** Collecter le SQL ? (profiler dev présent → buffer ORM alloué.) */
43
+ queries: boolean;
44
+ }
45
+ /**
46
+ * Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
47
+ * la connexion qui la porte.
48
+ *
49
+ * Pourquoi un objet séparé du `Context` : un `WebsocketContext` vit pour toute
50
+ * la **connexion**, alors qu'une socket peut porter des centaines d'invocations,
51
+ * concurrentes de surcroît. Empiler leurs phases sur le contexte produirait une
52
+ * timeline **cumulative** (donc fausse) et ferait croître `Context.phases` sans
53
+ * borne. Le profil naît et meurt donc avec la frame ; il voyage dans l'ALS
54
+ * (`RequestContext.payload.invocation`), seul canal déjà per-invocation traversé
55
+ * par le Resolver, le controller et les adapters ORM.
56
+ *
57
+ * Il satisfait **structurellement** ce que `Profiler.collect()` lit d'un
58
+ * contexte : aucune connaissance du transport n'est requise côté Profiler, un
59
+ * profil de frame s'y collecte comme une requête HTTP (`kind: "ws"`).
60
+ *
61
+ * Coût : **rien n'est alloué** quand le profiler dev est absent ET le timing
62
+ * éteint (production) — le pont n'ouvre alors aucun profil.
63
+ */
64
+ export declare class FrameProfile {
65
+ readonly requestId: string;
66
+ readonly type: string;
67
+ readonly scheme: string;
68
+ readonly method: string;
69
+ readonly url: string;
70
+ readonly remoteAddress: string | null;
71
+ readonly traceparent: string | null;
72
+ readonly security: ProfiledArea | null;
73
+ readonly securityTrace: ISecurityTrace | null;
74
+ /** Phases de CETTE frame (vide si le timing est éteint). */
75
+ readonly phases: PhaseTiming[];
76
+ /**
77
+ * Buffer ORM de CETTE frame — `null` hors profiling. C'est ce buffer que le
78
+ * kernel refusait d'allouer au handshake (il aurait cumulé N messages) : par
79
+ * invocation, il redevient exact, et le SQL se replace dans le waterfall.
80
+ */
81
+ readonly profilerQueries: IProfilerQuery[] | null;
82
+ /** Resolver de la frame — porte route / controller / action. */
83
+ resolver: ProfiledResolver | null;
84
+ /** Statut HTTP-équivalent de l'invocation (200, 403, 404…). */
85
+ response: {
86
+ statusCode: number;
87
+ } | null;
88
+ error: {
89
+ message: string;
90
+ } | null;
91
+ private readonly timing;
92
+ constructor(init: FrameProfileInit);
93
+ /**
94
+ * Ouvre une phase. Contrairement au `Context`, l'index est un **scan arrière**
95
+ * (pas de `Map<nom, index>`) : une frame porte une poignée de phases, et une
96
+ * phase peut être ré-entrante (une action qui en déclenche une autre) — la
97
+ * table par nom, elle, écraserait la première occurrence.
98
+ */
99
+ phaseStart(name: PhaseName): void;
100
+ /** Ferme la DERNIÈRE phase ouverte de ce nom (idempotent). */
101
+ phaseEnd(name: PhaseName): void;
102
+ /**
103
+ * Fige l'issue de l'invocation (statut + erreur éventuelle) avant collecte.
104
+ *
105
+ * @param status - statut HTTP-équivalent (200 ; 403/404/409… pour un refus).
106
+ * @param error - l'erreur qui a interrompu la frame, le cas échéant.
107
+ */
108
+ finish(status: number, error?: unknown): void;
109
+ }
110
+ export default FrameProfile;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Profiler — collecteur **dev-only** de profils de requête, indexé par `requestId`.
3
+ *
4
+ * Modèle inversé (≠ ancien monitoring-bundle qui splicait du Twig serveur dans
5
+ * le body) : le serveur **collecte** un instantané en fin de requête, le client
6
+ * (debug bar) **rend**. La matière existe déjà dans le `Context` quand le timing
7
+ * est actif (`phases`, `requestId`, `status`, resolver) → `collect()` ne fait
8
+ * que SNAPSHOTTER : aucune allocation per-request hors de cette structure, et
9
+ * **rien** en prod (le module n'instancie pas le Profiler hors dev).
10
+ *
11
+ * Stockage = ring buffer borné via l'ordre d'insertion d'une `Map` : à la
12
+ * capacité, on évince la plus ancienne entrée (première clé). O(1) amorti.
13
+ *
14
+ * Corrélation client↔serveur : le framework renvoie déjà `X-Request-Id` sur
15
+ * chaque réponse → le client lit le header de SON appel AJAX et fetch
16
+ * `/nodefony/profiler/api/{requestId}`. Zéro modif du pipeline.
17
+ *
18
+ * Multi-process : un profil vit sur le worker qui a traité l'appel (fetch même
19
+ * origine par requestId). Agrégat cluster = Redis (P13).
20
+ */
21
+ /** Une phase du pipeline, projetée pour le transport (durée résolue). */
22
+ export interface ProfilePhase {
23
+ name: string;
24
+ /** Début relatif `performance.now()` (ms) — sert au calcul du waterfall. */
25
+ startMs: number;
26
+ /** Durée en ms, `null` si la phase ne s'est pas terminée (erreur). */
27
+ durationMs: number | null;
28
+ }
29
+ /**
30
+ * Une requête SQL/NoSQL exécutée pendant la requête HTTP (SEAM ORM — futur).
31
+ *
32
+ * Non collecté aujourd'hui : quand `@nodefony/orm-core` arrivera, les adapters
33
+ * pousseront leurs requêtes dans un buffer per-request dev-only (via l'ALS
34
+ * `RequestContext.getRequestId()`, déjà en place) que `collect()` lira ici.
35
+ * Le champ `queries` reste donc `undefined` tant qu'aucun ORM ne pushe → 0 coût.
36
+ */
37
+ export interface ProfileQuery {
38
+ /** Requête (SQL ou commande NoSQL), tronquée si volumineuse. */
39
+ sql: string;
40
+ /** Début relatif (`performance.now()`) — même horloge que les phases. */
41
+ startMs?: number;
42
+ /** Durée d'exécution en ms. */
43
+ durationMs: number;
44
+ /** Lignes affectées/retournées, si connu. */
45
+ rows?: number;
46
+ /** Connecteur émetteur (`drizzle`, `mongoose`…). */
47
+ connector?: string;
48
+ }
49
+ /**
50
+ * Traversée du firewall par cette requête — **ce qui était possible** (la zone :
51
+ * son nom, si elle est protégée, quels authenticators elle accepte) croisé avec
52
+ * **ce qui s'est passé** (quel maillon a résolu l'identité, l'issue, le motif
53
+ * d'un refus).
54
+ *
55
+ * Sans cela, une requête qui PASSE est invisible côté sécurité : le chemin de
56
+ * succès n'émet aucun événement d'audit (choix délibéré — le volume nominal
57
+ * n'est pas un signal). `undefined` hors zone firewall.
58
+ */
59
+ export interface ProfileSecurity {
60
+ /** Nom de la zone traversée. */
61
+ zone: string | null;
62
+ /** La zone exige-t-elle une identité (`security: true`) ? */
63
+ protected: boolean;
64
+ /** Chaîne d'authenticators : `first` = le premier qui supporte, `all` = MFA. */
65
+ mode: string | null;
66
+ /** Authenticators que la zone accepte (ce qui était POSSIBLE). */
67
+ candidates: string[];
68
+ /** Authenticator qui a RÉELLEMENT résolu l'identité. */
69
+ authenticator: string | null;
70
+ /** `granted` · `anonymous` · `denied` · `failure` · `throttled` · `bypass` · `public`. */
71
+ outcome: string | null;
72
+ /** Motif du refus (`no_credentials`, `invalid_credentials`…). */
73
+ reason: string | null;
74
+ /** Rôles du token résolu (l'axe autorisation). */
75
+ roles: string[] | null;
76
+ }
77
+ /** Profil complet d'une requête (HTTP ou message WS), exposé par `get`. */
78
+ export interface ProfileEntry {
79
+ requestId: string;
80
+ /** Horodatage de la collecte (`Date.now()`), pour le tri client. */
81
+ ts: number;
82
+ kind: "http" | "ws";
83
+ method: string | null;
84
+ url: string;
85
+ scheme: string;
86
+ status: number | null;
87
+ /** Durée totale serveur (ms), dérivée des phases. */
88
+ durationMs: number | null;
89
+ route: string | null;
90
+ controller: string | null;
91
+ action: string | null;
92
+ remoteAddress: string | null;
93
+ /** Identité (username) si le firewall l'a injectée, sinon `null`. */
94
+ user: string | null;
95
+ /** `traceparent` W3C Trace Context (P2.7) — corrélation distribuée RFC-propre. */
96
+ traceparent: string | null;
97
+ error: string | null;
98
+ phases: ProfilePhase[];
99
+ /** Requêtes ORM (SEAM futur — `undefined` tant qu'aucun adapter ne pushe). */
100
+ queries?: ProfileQuery[];
101
+ /** Traversée du firewall — `undefined` si la requête n'a croisé aucune zone. */
102
+ security?: ProfileSecurity;
103
+ }
104
+ /** Résumé léger d'un profil pour la liste `recent` (sans les phases). */
105
+ export interface ProfileSummary {
106
+ requestId: string;
107
+ ts: number;
108
+ kind: "http" | "ws";
109
+ method: string | null;
110
+ url: string;
111
+ status: number | null;
112
+ durationMs: number | null;
113
+ route: string | null;
114
+ error: string | null;
115
+ }
116
+ /** Forme structurelle minimale lue sur un `Context` (lecture défensive). */
117
+ interface ProfilableContext {
118
+ requestId?: string;
119
+ type?: string;
120
+ scheme?: string;
121
+ method?: string | null;
122
+ url?: string;
123
+ remoteAddress?: string | null;
124
+ traceparent?: string | null;
125
+ error?: {
126
+ message?: string;
127
+ } | null;
128
+ response?: {
129
+ statusCode?: number;
130
+ } | null;
131
+ phases?: ReadonlyArray<{
132
+ name: string;
133
+ startMs: number;
134
+ durationMs?: number;
135
+ }>;
136
+ resolver?: {
137
+ route?: {
138
+ name?: string;
139
+ } | null;
140
+ controller?: {
141
+ name?: string;
142
+ } | null;
143
+ actionName?: string;
144
+ } | null;
145
+ /**
146
+ * Buffer de requêtes ORM rempli pendant la requête (dev-only). Même tableau
147
+ * que la payload ALS — les adapters ORM y poussent via `RequestContext`.
148
+ * `null`/absent hors profiling.
149
+ */
150
+ profilerQueries?: ProfileQuery[] | null;
151
+ /**
152
+ * Zone firewall capturée (`SecuredArea`, posée par `Firewall.isSecure`). Lue
153
+ * en structurel : `@nodefony/http` ne peut pas importer `@nodefony/security`.
154
+ */
155
+ security?: {
156
+ name?: string;
157
+ security?: boolean;
158
+ mode?: string;
159
+ authenticators?: readonly string[];
160
+ } | null;
161
+ /** Décision du firewall sur cette requête (dev-only, cf `ISecurityTrace`). */
162
+ securityTrace?: {
163
+ authenticator: string | null;
164
+ outcome: string;
165
+ reason: string | null;
166
+ user: string | null;
167
+ roles: string[] | null;
168
+ } | null;
169
+ }
170
+ export declare class Profiler {
171
+ private readonly _buf;
172
+ private readonly _cap;
173
+ constructor(cap?: number);
174
+ /**
175
+ * Snapshot un `Context` en fin de requête. Tolérant aux champs absents
176
+ * (handshake WS, erreur précoce). À appeler AVANT `context.clean()`.
177
+ *
178
+ * @param ctx - le Context HTTP/WS terminé (lu en structurel, jamais muté).
179
+ */
180
+ collect(ctx: ProfilableContext): void;
181
+ /** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
182
+ get(requestId: string): ProfileEntry | undefined;
183
+ /**
184
+ * Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
185
+ */
186
+ recent(limit?: number): ProfileSummary[];
187
+ /** Nombre de profils en mémoire. */
188
+ get size(): number;
189
+ /** Vide le ring buffer. */
190
+ clear(): void;
191
+ }
192
+ export default Profiler;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Générateurs de configuration reverse-proxy (nginx / haproxy) DÉRIVÉE de
3
+ * l'introspection Nodefony — fonctions PURES (aucun accès kernel/fs), testables
4
+ * et sérialisables. Le câblage (lecture des services) vit dans la commande CLI
5
+ * `proxy:generate` ; ici on ne fait que transformer un modèle → texte de conf.
6
+ *
7
+ * Résout le « trou statiques multi-modules » : Nodefony sert N dossiers `public/`
8
+ * (racine + un par module) AU MÊME préfixe `/`. nginx ne sait pas servir N roots
9
+ * sous `/` → on génère une CHAÎNE de `try_files` via des locations nommées
10
+ * (root d0 → @r1 → root d1 → … → @nodefony), fallback final vers le backend.
11
+ */
12
+ /** Un dossier statique servi sous un préfixe d'URL (mount préfixé). */
13
+ export interface ProxyStaticMount {
14
+ /** Préfixe d'URL (ex. `/_assets/studio/`). */
15
+ prefix: string;
16
+ /** Dossier absolu servi. */
17
+ dir: string;
18
+ }
19
+ /** Modèle d'introspection consommé par les générateurs. */
20
+ export interface ProxyIntrospection {
21
+ /** `server_name` (hôtes de confiance, IP exclues). Vide → `_` (catch-all). */
22
+ domains: string[];
23
+ /** Hôte du backend Nodefony à joindre (ex. `host.docker.internal`). */
24
+ backendHost: string;
25
+ /** Port HTTP du backend (clair). */
26
+ httpPort: number;
27
+ /** Port HTTPS/2 du backend (re-encrypt). */
28
+ httpsPort: number;
29
+ /** Dossiers statiques servis à la racine `/` (ordre = priorité). */
30
+ staticRoots: string[];
31
+ /** Montages statiques préfixés (servis tels quels). */
32
+ mounts: ProxyStaticMount[];
33
+ /** Port d'écoute du proxy généré (défaut 80). */
34
+ listen: number;
35
+ /** Re-chiffrer vers le backend HTTPS (true) ou forward en clair (false). */
36
+ reencrypt: boolean;
37
+ /**
38
+ * Taille maximale d'un corps de requête acceptée par Nodefony, en octets
39
+ * (`http.maxBodySize`). `0` = ne rien imposer au proxy.
40
+ *
41
+ * Sans elle, nginx applique son propre défaut — **1 Mo** — et rend un `413`
42
+ * que le serveur ne voit jamais : l'application marche en direct et casse
43
+ * derrière le proxy, sur une limite que personne n'a écrite.
44
+ */
45
+ maxBodyBytes: number;
46
+ /**
47
+ * Intervalle du heartbeat WebSocket, en millisecondes (`ws.keepaliveInterval`,
48
+ * `0` = désactivé) — d'où les proxys tirent leur délai de tunnel.
49
+ *
50
+ * Une WebSocket est, vue d'un proxy, une connexion SANS trafic entre deux
51
+ * messages. Ce qui la garde en vie derrière nginx et haproxy, ce sont les
52
+ * pings du serveur : le délai d'inactivité doit donc être dérivé de leur
53
+ * intervalle, jamais posé au hasard. Heartbeat désactivé → plus rien ne borne
54
+ * le silence, et seul un délai franchement long évite de couper des sockets
55
+ * saines.
56
+ */
57
+ keepaliveIntervalMs: number;
58
+ }
59
+ /** Valeurs par défaut d'un modèle d'introspection (complété par la commande). */
60
+ export declare const defaultIntrospection: ProxyIntrospection;
61
+ /**
62
+ * Génère une configuration nginx complète (reverse-proxy + offload statiques).
63
+ *
64
+ * @param intro - modèle d'introspection Nodefony.
65
+ * @returns le contenu d'un `nginx.conf`.
66
+ */
67
+ export declare function generateNginxConfig(intro: ProxyIntrospection): string;
68
+ /**
69
+ * Génère une configuration haproxy (reverse-proxy + Forwarded RFC 7239).
70
+ * haproxy ne sert PAS de fichiers : les statiques sont proxifiés au backend
71
+ * (ou désactivés côté serveur via `statics.enabled: false` + un nginx/CDN).
72
+ *
73
+ * @param intro - modèle d'introspection Nodefony.
74
+ * @returns le contenu d'un `haproxy.cfg`.
75
+ */
76
+ export declare function generateHaproxyConfig(intro: ProxyIntrospection): string;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Contrat d'un compteur de **rate-limit général par IP** (P0.3) et son verdict.
3
+ *
4
+ * À NE PAS confondre avec `security.rateLimit` (backoff de LOGIN NIST, par
5
+ * identifiant saisi). Ici : plafond de trafic par IP cliente, sur TOUTES les
6
+ * routes HTTP, matérialisé par les en-têtes `X-RateLimit-*` + un `429` (RFC 6585).
7
+ *
8
+ * Le contrat est **SYNCHRONE** à dessein : `hit()` est appelé sur le hot-path de
9
+ * CHAQUE requête → aucune `Promise`, aucune microtask. Un futur store distribué
10
+ * (Redis, multi-pod) introduira son propre chemin d'exécution, pas ce contrat.
11
+ */
12
+ import type { IPage, IPageQuery } from "nodefony";
13
+ /**
14
+ * Verdict rendu par {@link IRateLimitStore.hit} — porte tout le nécessaire pour
15
+ * émettre les en-têtes `X-RateLimit-*` (+ `Retry-After` en cas de rejet) sans
16
+ * relire l'état du store.
17
+ */
18
+ export interface RateLimitVerdict {
19
+ /** `true` si la requête dépasse le quota de la fenêtre → réponse `429`. */
20
+ readonly limited: boolean;
21
+ /** Plafond de la fenêtre — en-tête `X-RateLimit-Limit`. */
22
+ readonly limit: number;
23
+ /** Requêtes restantes dans la fenêtre courante (`X-RateLimit-Remaining`, ≥ 0). */
24
+ readonly remaining: number;
25
+ /** Fin de la fenêtre courante, en **ms epoch** (`X-RateLimit-Reset` = `⌈/1000⌉`). */
26
+ readonly resetAtMs: number;
27
+ /** Secondes jusqu'au reset (`Retry-After`) ; `0` si la requête n'est pas limitée. */
28
+ readonly retryAfterS: number;
29
+ }
30
+ /** Options de construction d'un {@link IRateLimitStore}. */
31
+ export interface IRateLimitOptions {
32
+ /** Largeur de la fenêtre fixe, en **millisecondes**. */
33
+ readonly windowMs: number;
34
+ /** Nombre max de requêtes autorisées par clé (IP) et par fenêtre. */
35
+ readonly max: number;
36
+ /** Borne mémoire : nombre max de clés (IP) suivies simultanément. */
37
+ readonly maxTracked: number;
38
+ }
39
+ /**
40
+ * Une clé (IP) actuellement suivie, telle qu'exposée à l'INTROSPECTION admin —
41
+ * l'état, jamais le trafic : ni URL, ni en-tête, ni corps. Une IP reste une
42
+ * donnée personnelle → ce listing est réservé au data plane admin.
43
+ */
44
+ export interface IRateLimitEntry {
45
+ /** Clé suivie (IP cliente résolue). */
46
+ readonly key: string;
47
+ /** Hits comptés dans la fenêtre courante. */
48
+ readonly count: number;
49
+ /** Fin de la fenêtre courante (ms epoch). */
50
+ readonly resetAtMs: number;
51
+ /** `true` si la clé a dépassé le plafond de la fenêtre (elle prend des 429). */
52
+ readonly limited: boolean;
53
+ }
54
+ /**
55
+ * Requête de listing des clés suivies — {@link IPageQuery} + le seul filtre qui
56
+ * a un sens ici. `q` (hérité) = préfixe de clé (« 10.0. » pour un sous-réseau),
57
+ * pas une sous-chaîne : sur une IP, seul le préfixe est signifiant.
58
+ */
59
+ export interface IRateLimitListQuery extends IPageQuery {
60
+ /** `true` = seulement les clés au plafond, `false` = seulement les autres. */
61
+ limited?: boolean;
62
+ }
63
+ /**
64
+ * Compteur de rate-limit par clé (IP). Implémentation par défaut :
65
+ * {@link import("./MemoryRateLimitStore").MemoryRateLimitStore} (fenêtre fixe,
66
+ * en mémoire).
67
+ */
68
+ export interface IRateLimitStore {
69
+ /**
70
+ * Enregistre un hit pour `key` (IP cliente résolue) et renvoie le verdict de
71
+ * la fenêtre courante. Synchrone, O(1).
72
+ */
73
+ hit(key: string): RateLimitVerdict;
74
+ /**
75
+ * Purge les fenêtres expirées (`resetAt <= now`). Appelé hors hot-path par le
76
+ * `GcScheduler` du core.
77
+ *
78
+ * @param nowMs - horloge injectable (ms epoch) ; défaut = horloge du store.
79
+ * @returns le nombre d'entrées purgées.
80
+ */
81
+ gc(nowMs?: number): number;
82
+ /**
83
+ * Page des clés suivies — introspection admin (« qui me martèle ? »). Ne
84
+ * matérialise jamais plus d'une page, filtres appliqués au store.
85
+ *
86
+ * **Asynchrone** alors que {@link IRateLimitStore.hit} est synchrone : ce
87
+ * listing est hors hot-path (console admin), et un futur store distribué le
88
+ * servira par `SCAN`. La contrainte « zéro Promise » ne vaut que pour `hit`.
89
+ *
90
+ * Ordre contractuel : `count` DESC (les plus bruyants d'abord — c'est LA
91
+ * question d'exploitation), départagé par `key` ASC.
92
+ */
93
+ listPage(query: IRateLimitListQuery): Promise<IPage<IRateLimitEntry>>;
94
+ /** Nombre de clés (IP) actuellement suivies — introspection / métrique. */
95
+ readonly trackedCount: number;
96
+ /** Total cumulé de requêtes rejetées (`429`) depuis le boot — métrique. */
97
+ readonly rejectedTotal: number;
98
+ }
@@ -0,0 +1,40 @@
1
+ import type { IPage } from "nodefony";
2
+ import type { IRateLimitEntry, IRateLimitListQuery, IRateLimitOptions, IRateLimitStore, RateLimitVerdict } from "./IRateLimitStore.js";
3
+ /**
4
+ * Store de rate-limit **en mémoire** — algorithme *fixed window* par clé (IP).
5
+ *
6
+ * Une entrée par IP `{ count, resetAt }` ; à l'expiration de la fenêtre le
7
+ * compteur repart à zéro (mutation in-place, 0 alloc pour une IP récurrente).
8
+ * `hit()` est O(1) (1 `Map.get` + arithmétique), et la `Map` est allouée en
9
+ * **lazy** au 1ᵉʳ hit → 0 coût mémoire si le rate-limit n'est jamais sollicité.
10
+ *
11
+ * Mémoire **bornée** par `maxTracked` : au cap, on purge d'abord les fenêtres
12
+ * expirées puis on évince en FIFO (ordre d'insertion `Map`). Un {@link gc}
13
+ * planifiable (GcScheduler du core) fait le ménage hors hot-path.
14
+ *
15
+ * ⚠️ Limite assumée (fenêtre fixe) : un pic à cheval sur deux fenêtres peut
16
+ * laisser passer jusqu'à `2 × max` sur un court intervalle. Acceptable pour une
17
+ * défense de capacité ; un *sliding window* viendrait en option si nécessaire.
18
+ */
19
+ export declare class MemoryRateLimitStore implements IRateLimitStore {
20
+ #private;
21
+ /**
22
+ * @param options - fenêtre, plafond, borne mémoire.
23
+ * @param now - horloge injectable (ms) — `Date.now` par défaut, surchargée en test.
24
+ */
25
+ constructor(options: IRateLimitOptions, now?: () => number);
26
+ hit(key: string): RateLimitVerdict;
27
+ gc(nowMs?: number): number;
28
+ /**
29
+ * {@inheritDoc IRateLimitStore.listPage}
30
+ *
31
+ * La collection est déjà en RAM et **bornée par `maxTracked`** (c'est la
32
+ * nature de ce store) : le tri porte sur des références, seule la page est
33
+ * matérialisée en objets de sortie. Les fenêtres expirées sont exclues à la
34
+ * lecture — les montrer ferait passer un compteur mort pour du trafic vivant
35
+ * (le `gc` les retire plus tard, hors hot-path).
36
+ */
37
+ listPage(query: IRateLimitListQuery): Promise<IPage<IRateLimitEntry>>;
38
+ get trackedCount(): number;
39
+ get rejectedTotal(): number;
40
+ }