@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,283 @@
1
+ import type { IPage, IPageQuery, ISortableSource } from "nodefony";
2
+ import type { ICookie, ICookieOptions } from "./ICookie.js";
3
+ export type SessionStatusType = "none" | "active" | "disabled";
4
+ export type SessionStrategyType = "none" | "migrate" | "invalidate";
5
+ export type FlashBagType = Record<string, unknown>;
6
+ export type MetaBagType = Record<string, unknown>;
7
+ /**
8
+ * Intent d'activation de session déclaré par une route (décorateur `@UseSession`
9
+ * de `@nodefony/framework`, ou présence d'un paramètre `@Session`). Lu au point
10
+ * d'activation **unique** du pipeline (HTTP comme WS) : c'est lui — et non plus un
11
+ * `sessionAutoStart` global « démarre partout » — qui décide d'ouvrir une session.
12
+ *
13
+ * - `readOnly` : la session est lue/reprise mais **jamais persistée** (0 write storage).
14
+ */
15
+ export interface SessionIntent {
16
+ readOnly?: boolean;
17
+ }
18
+ /**
19
+ * Données de session **sérialisées** échangées avec un {@link ISessionStorage}
20
+ * (blob opaque persisté/restauré). La forme métier riche (ProtoService/bags) est
21
+ * l'affaire de `Session` ; le storage ne manipule que cette projection JSON-safe.
22
+ */
23
+ export interface ISerializedSession {
24
+ Attributes: Record<string, unknown>;
25
+ metaBag: Record<string, unknown>;
26
+ flashBag: Record<string, unknown>;
27
+ user: string;
28
+ createdAt?: Date;
29
+ updatedAt?: Date;
30
+ }
31
+ /**
32
+ * Projection **redactée** d'une session pour l'ADMINISTRATION (data plane Studio,
33
+ * `/nodefony/http/api/sessions`). Construite par allowlist — **jamais** par `delete`
34
+ * après coup — donc ne porte par construction ni `Attributes` ni `flashBag` (données
35
+ * métier potentiellement sensibles), ni l'**id de session brut** (= la valeur du
36
+ * cookie ; le posséder = être connecté).
37
+ *
38
+ * À la place : {@link ISessionSummary.ref}, pseudonyme HMAC stable et **non
39
+ * réversible** (cf `SessionsService.sessionRef`). Standard « appareils connectés »
40
+ * GitHub/Google : on montre une référence, jamais le jeton de session.
41
+ */
42
+ export interface ISessionSummary {
43
+ /** Pseudonyme `HMAC(secret, id)` tronqué (préfixe `sess_…`). JAMAIS l'id brut. */
44
+ ref: string;
45
+ /** Identifiant de l'utilisateur porté par la session (chaîne vide = anonyme). */
46
+ user: string;
47
+ /** Vrai si la session porte un utilisateur authentifié (`user` non vide). */
48
+ authenticated: boolean;
49
+ /** IP capturée au login (`metaBag.ip`), ou `null` si non capturée / anonyme. */
50
+ ip: string | null;
51
+ /** User-Agent capturé au login (`metaBag.ua`), ou `null` si non capturé. */
52
+ ua: string | null;
53
+ /** Création de la session (epoch ms), ou `null` si inconnue. */
54
+ createdAt: number | null;
55
+ /** Dernière persistance (epoch ms), ou `null` si inconnue. */
56
+ updatedAt: number | null;
57
+ /** Réserve multi-tenant (toujours `null` en mono-tenant — slot coût-0). */
58
+ tenantId: string | null;
59
+ /**
60
+ * Vrai si cette entrée est **la session qui porte la requête en cours** — le
61
+ * « cet appareil » des consoles d'appareils connectés.
62
+ *
63
+ * Sans elle, un client qui liste ses sessions ne peut désigner AUCUNE ligne :
64
+ * ni celle qu'il ne doit pas fermer, ni celle qu'il veut fermer. Deux
65
+ * conséquences vécues, la même cause : la console d'administration marquait
66
+ * « vous appartient » toutes les lignes (comparaison sur l'UTILISATEUR, jamais
67
+ * sur la session), et un banc révoquait « la sienne » en prenant la première
68
+ * de la liste — c'est-à-dire la plus récente du COMPTE, celle d'un voisin dès
69
+ * que deux clients partagent l'identité.
70
+ *
71
+ * `false` quand la requête ne porte pas de session (invocation CLI, appel
72
+ * interne) : l'énumération est alors faite par personne, aucune ligne n'est
73
+ * « celle-ci ».
74
+ */
75
+ current: boolean;
76
+ }
77
+ /**
78
+ * Entrée **brute** d'énumération renvoyée par {@link ISessionStorage.listAll} :
79
+ * l'id opaque RÉEL + le blob sérialisé. Usage strictement **interne au process**
80
+ * (le service en a besoin pour calculer le `ref` et révoquer par id) — n'est
81
+ * JAMAIS sérialisée vers une réponse HTTP : `SessionsService` la projette en
82
+ * {@link ISessionSummary} (redaction par construction).
83
+ *
84
+ * Optimisation côté stores SQL/NoSQL : `data.Attributes`/`data.flashBag` peuvent
85
+ * être renvoyés vides (les secrets ne quittent alors jamais la base) — seuls
86
+ * `user`/`metaBag`/timestamps sont nécessaires à la projection.
87
+ */
88
+ export interface ISessionRecord {
89
+ /** Identifiant opaque réel de la session (interne — jamais exposé via l'API). */
90
+ id: string;
91
+ data: ISerializedSession;
92
+ }
93
+ /**
94
+ * Filtre d'énumération admin de {@link ISessionStorage.listAll}. Tous les champs
95
+ * sont optionnels. Un store SQL peut honorer `user` (WHERE indexable) ; les autres
96
+ * peuvent l'ignorer — `SessionsService.listAllSessions` ré-applique le filtre de
97
+ * façon défensive. `tenantId` = slot multi-tenant (ignoré en mono-tenant).
98
+ */
99
+ export interface ISessionListFilter {
100
+ /** Restreint aux sessions d'un utilisateur (pour « déconnecter partout »). */
101
+ user?: string;
102
+ /** Slot multi-tenant — non scopé aujourd'hui (réserve coût-0). */
103
+ tenantId?: string | null;
104
+ }
105
+ /**
106
+ * Requête d'énumération **paginée** des sessions — le {@link IPageQuery} standard
107
+ * de Nodefony étendu des filtres propres au store de session. C'est la forme que
108
+ * consomme {@link ISessionStorage.listPage} ; `ISessionListFilter` reste la forme
109
+ * non paginée du dump {@link ISessionStorage.listAll}.
110
+ *
111
+ * **Filtres portables par construction** (`user` = égalité, `authenticated` =
112
+ * `user` non vide) : ils s'expriment dans tous les backends — `WHERE` SQL/Mongo
113
+ * indexable, prédicat mémoire — donc aucun n'oblige un store à matérialiser la
114
+ * collection pour filtrer.
115
+ *
116
+ * **Tri** : l'ordre du contrat est `updatedAt` DESC (session la plus récemment
117
+ * active d'abord), départagé par l'id pour rester **déterministe** à horodatage
118
+ * égal (deux sessions écrites dans la même milliseconde). Un backend curseur
119
+ * (Redis `SCAN`) n'a pas d'ordre global — il l'annonce, il ne le simule pas.
120
+ */
121
+ export interface ISessionListQuery extends IPageQuery, ISessionListFilter {
122
+ /**
123
+ * Restreint aux sessions **authentifiées** (`true` : `user` non vide) ou
124
+ * **anonymes** (`false`). Omis = les deux. Sert les KPI de la console admin
125
+ * sans jamais énumérer (cf {@link ISessionStorage.countSessions}).
126
+ */
127
+ authenticated?: boolean;
128
+ }
129
+ /**
130
+ * Contrat **unique** d'un backend de stockage de session (File, Redis, SQL/Drizzle…).
131
+ * Source de vérité unifiée — l'ex-doublon `sessionStorageInterface` (any) n'est plus
132
+ * qu'un alias transitionnel. Enregistré dans le registre IoC `SessionsService.registerStorage`.
133
+ */
134
+ export interface ISessionStorage extends ISortableSource {
135
+ read(id: string): Promise<ISerializedSession>;
136
+ write(id: string, data: ISerializedSession): Promise<ISerializedSession>;
137
+ start(id: string): Promise<ISerializedSession>;
138
+ open(): Promise<number>;
139
+ close(): boolean;
140
+ destroy(id: string): Promise<boolean>;
141
+ /**
142
+ * Purge les sessions expirées sur les **deux bornes** NIST/OWASP : idle
143
+ * (inactivité depuis `updatedAt`) ET absolute (âge depuis `createdAt`, jamais
144
+ * prolongé). `absoluteSeconds` omis/0 → seul l'idle s'applique. Hors hot-path
145
+ * (timer `GcScheduler`). Un store à TTL natif (Redis) peut le laisser no-op
146
+ * pour l'idle — l'absolute restant honoré à la lecture (`isValidSession`).
147
+ */
148
+ gc(idleSeconds?: number, absoluteSeconds?: number): Promise<void>;
149
+ /**
150
+ * **Prolonge l'idle timeout** d'une session active (timeout glissant) SANS
151
+ * réécrire son blob — `UPDATE updatedAt` (SQL), `EXPIRE` (Redis TTL), `utimes`
152
+ * (fichier). Capacité **optionnelle** : un store qui ne l'implémente pas voit
153
+ * son idle prolongé uniquement par un vrai `write` (mutation) → dégradation
154
+ * gracieuse, jamais un breaking.
155
+ *
156
+ * Appelé de façon **throttlée** (1 fois par tranche d'idle, jamais par requête)
157
+ * sur l'activité HTTP/WS — y compris en lecture seule — pour qu'une session
158
+ * réellement utilisée n'expire pas. N'affecte **jamais** l'absolute timeout
159
+ * (borné à la création).
160
+ *
161
+ * @param id - identifiant opaque de la session.
162
+ * @param idleSeconds - idle courant (les stores à TTL natif, ex. Redis, en ont
163
+ * besoin pour repositionner l'expiration).
164
+ */
165
+ touch?(id: string, idleSeconds?: number): Promise<void>;
166
+ /**
167
+ * Énumère les sessions persistées — capacité d'**ADMINISTRATION** (gouvernance,
168
+ * « déconnecter partout »), **optionnelle**. Un backend incapable de lister
169
+ * (KV sans scan, edge…) l'omet : l'endpoint admin répond alors **501** (refus
170
+ * honnête), jamais une liste vide trompeuse.
171
+ *
172
+ * Renvoie des {@link ISessionRecord} bruts (id réel + blob) — le service les
173
+ * redacte. Coût **O(N)** assumé (admin, faible fréquence, pas de hot-path) ;
174
+ * un index inverse `user → [id]` reste une optimisation future.
175
+ *
176
+ * @param filter - restriction optionnelle (ex. `user`) ; un store peut l'honorer
177
+ * (WHERE SQL) ou l'ignorer (le service ré-applique).
178
+ */
179
+ listAll?(filter?: ISessionListFilter): Promise<ISessionRecord[]>;
180
+ /**
181
+ * Énumère **une page** de sessions — la capacité d'administration NORMALE
182
+ * (console, « mes appareils », révocation). Contrairement à {@link listAll},
183
+ * un store conforme ne matérialise **jamais** plus d'une page : `LIMIT/OFFSET`
184
+ * SQL, `skip/limit` Mongo, `SCAN` par curseur Redis, tranche mémoire. C'est ce
185
+ * qui rend le coût d'une requête admin indépendant du nombre de sessions.
186
+ *
187
+ * Optionnelle, comme {@link listAll} : un backend incapable d'énumérer l'omet
188
+ * → l'endpoint admin répond **501** (refus honnête), jamais une page vide
189
+ * trompeuse.
190
+ *
191
+ * **Redaction par construction — garantie du contrat, pas une optimisation** :
192
+ * les records rendus portent `Attributes` et `flashBag` **vides**. Les données
193
+ * métier d'une session (potentiellement des secrets) n'ont aucune raison de
194
+ * traverser la couche d'administration : les stores SQL/NoSQL ne les
195
+ * sélectionnent pas, les stores mémoire ne les recopient pas. Un appelant qui
196
+ * oublierait la projection en {@link ISessionSummary} ne peut donc pas les
197
+ * faire fuiter. (Le dump {@link listAll}, lui, reste libre de les porter.)
198
+ *
199
+ * **Deux modes, déclarés par le store dans sa réponse** :
200
+ * - **offset** — `total` exact (sauf `withTotal:false`) et ordre `updatedAt`
201
+ * DESC déterministe ; `nextCursor` absent.
202
+ * - **curseur** — `nextCursor` à repasser en {@link IPageQuery.cursor}, pas de
203
+ * `total` ni d'ordre global, taille de page variable. Le client boucle
204
+ * jusqu'à `nextCursor === null`. Capacité réduite **assumée**, pas simulée.
205
+ *
206
+ * @param query - page + filtres ({@link ISessionListQuery}).
207
+ * @returns la page de {@link ISessionRecord} bruts — le service les redacte.
208
+ */
209
+ listPage?(query: ISessionListQuery): Promise<IPage<ISessionRecord>>;
210
+ /**
211
+ * Compte les sessions correspondant aux filtres, **sans les énumérer** (`COUNT`
212
+ * natif SQL/Mongo). Alimente les KPI de la console (total, authentifiées vs
213
+ * anonymes) sans jamais charger de collection.
214
+ *
215
+ * @param query - les **filtres** seuls. Un comptage n'a pas de fenêtre : `limit`
216
+ * et `offset` n'y ont aucun sens, d'où le `Partial` (le contrat de page rend
217
+ * `limit` obligatoire, ce qui obligerait l'appelant à inventer une valeur
218
+ * ignorée).
219
+ * @returns le total exact, ou **`-1`** si le backend ne sait pas compter à coût
220
+ * raisonnable (Redis : compter = re-`SCAN` tout le keyspace). `-1` est un
221
+ * « je ne sais pas » explicite — l'appelant affiche l'inconnu, il ne l'invente pas.
222
+ */
223
+ countSessions?(query?: Partial<ISessionListQuery>): Promise<number>;
224
+ /**
225
+ * Compte les utilisateurs **distincts** portant au moins une session active.
226
+ *
227
+ * Ce n'est pas un {@link countSessions} avec un filtre de plus : dédupliquer
228
+ * exige de regrouper, là où compter n'exige que de parcourir. C'est pourquoi
229
+ * la capacité vit ici, sur le store, et non dans la table de facettes de la
230
+ * ressource — un `COUNT(DISTINCT …)` est trivial en SQL, direct en Mongo, et
231
+ * hors de portée d'un backend en curseur.
232
+ *
233
+ * Sépare deux nombres que la console confondrait sinon : « 400 sessions » et
234
+ * « 12 personnes connectées » ne racontent pas la même chose sur un parc.
235
+ *
236
+ * @param query - mêmes filtres que {@link countSessions} ; le décompte porte
237
+ * sur le sous-ensemble filtré.
238
+ * @returns le nombre d'utilisateurs distincts, ou **`-1`** si le backend ne
239
+ * sait pas agréger (même convention que {@link countSessions}).
240
+ */
241
+ countDistinctUsers?(query?: Partial<ISessionListQuery>): Promise<number>;
242
+ }
243
+ export interface ISession {
244
+ id: string;
245
+ name: string;
246
+ status: SessionStatusType;
247
+ saved: boolean;
248
+ /** Vrai si la session a été mutée sans être encore persistée (dirty-tracking). */
249
+ dirty: boolean;
250
+ /** Lecture seule : la session est reprise mais jamais persistée (0 write storage). */
251
+ readOnly: boolean;
252
+ migrated: boolean;
253
+ cookieSession: ICookie | null | undefined;
254
+ flashBag: FlashBagType;
255
+ strategy: SessionStrategyType;
256
+ created?: Date;
257
+ updated?: Date;
258
+ user?: string;
259
+ lifetime?: number;
260
+ storage: ISessionStorage;
261
+ start(context: unknown): Promise<ISession>;
262
+ save(user?: string): Promise<ISession>;
263
+ invalidate(lifetime?: number, id?: string, options?: ICookieOptions): Promise<ISession>;
264
+ destroy(cookieDelete?: boolean): Promise<boolean>;
265
+ create(lifetime: number, id?: string, options?: ICookieOptions): ISession;
266
+ /** Régénère un identifiant opaque CSPRNG en conservant l'état (anti session-fixation, appelée au login par `AuthFlow`). */
267
+ regenerateId(): void;
268
+ get(key: string): unknown;
269
+ set(key: string, value: unknown): unknown;
270
+ getAttributes(): unknown;
271
+ getMetaBag(key: string): unknown;
272
+ setMetaBag(key: string, value: unknown): unknown;
273
+ getMetas(): MetaBagType;
274
+ getFlashBag(key: string): unknown;
275
+ setFlashBag(key: string, value: unknown): unknown;
276
+ flashBags(): FlashBagType;
277
+ clearFlashBag(key: string): void;
278
+ clearFlashBags(): void;
279
+ getName(): string;
280
+ checkStatus(): "restart" | boolean;
281
+ serialize(user?: string): ISerializedSession;
282
+ deSerialize(data: ISerializedSession): void;
283
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Forme neutre d'un fichier multipart parsé (temp écrit sur disque), découplée
3
+ * du parser sous-jacent. Remplace l'ex-couplage à `formidable.File` : le moteur
4
+ * de parsing (busboy) n'apparaît plus dans le contrat consommé par
5
+ * `UploadedFile`. Champs alignés sur ce que lit `UploadedFile.create`.
6
+ */
7
+ export interface IParsedUploadFile {
8
+ /** Chemin absolu du fichier temporaire écrit sur disque. */
9
+ filepath: string;
10
+ /** Nom de fichier temporaire généré (UUID + extension). */
11
+ newFilename: string;
12
+ /** Nom de fichier d'origine déclaré par le client (peut être null). */
13
+ originalFilename: string | null;
14
+ /** Type MIME déclaré dans la part multipart. */
15
+ mimetype: string | null;
16
+ /** Taille réellement écrite sur disque (octets). */
17
+ size: number;
18
+ /** Date d'écriture du temporaire. */
19
+ mtime: Date | null;
20
+ /** Algorithme de hash appliqué pendant le stream, ou `false` si aucun. */
21
+ hashAlgorithm: false | "sha1" | "md5" | "sha256";
22
+ /** Hash hexadécimal du contenu si `hashAlgorithm` configuré, sinon null. */
23
+ hash: string | null;
24
+ }
25
+ /**
26
+ * Options du sous-système d'upload (clé de config `upload`). Mappées sur les
27
+ * `limits` de busboy + la gestion temp/hash propre à Nodefony.
28
+ */
29
+ export interface IUploadOptions {
30
+ /** Répertoire de dépôt des fichiers temporaires. */
31
+ uploadDir?: string;
32
+ /** Taille max d'UN fichier (octets) — `limits.fileSize` busboy. */
33
+ maxFileSize?: number;
34
+ /** Taille max CUMULÉE des fichiers d'une requête (octets) — appliquée par Nodefony. */
35
+ maxTotalFileSize?: number;
36
+ /** Nombre max de fichiers — `limits.files` busboy. */
37
+ maxFiles?: number;
38
+ /** Nombre max de champs texte — `limits.fields` busboy. */
39
+ maxFields?: number;
40
+ /** Taille max d'un champ texte (octets) — `limits.fieldSize` busboy. */
41
+ maxFieldsSize?: number;
42
+ /** Hash calculé pendant le stream (défaut `false` = aucun). */
43
+ hashAlgorithm?: false | "sha1" | "md5" | "sha256";
44
+ /** Encodage par défaut des parts texte. */
45
+ encoding?: string;
46
+ }
47
+ export interface IUploadedFile {
48
+ filename: string;
49
+ size: number;
50
+ prettySize: string;
51
+ mimeType: string | null | undefined;
52
+ hash: string | null | undefined;
53
+ hashAlgorithm: false | "sha1" | "md5" | "sha256";
54
+ lastModifiedDate: Date | null | undefined;
55
+ move(target: string): IUploadedFile;
56
+ /** Variante non bloquante de `move()` (recommandée dans le pipeline). */
57
+ moveAsync(target: string): Promise<IUploadedFile>;
58
+ getMimeType(): string | null | undefined;
59
+ getSize(): number;
60
+ getPrettySize(): string;
61
+ }
62
+ export interface IUploadService {
63
+ path?: string | unknown;
64
+ /** Construit un `UploadedFile` en async (stat non bloquant). */
65
+ createUploadFile(file: unknown, name: string): Promise<IUploadedFile>;
66
+ }
@@ -0,0 +1,7 @@
1
+ export type { ICookie, ICookieOptions, IWsCookie, SameSiteType, PriorityType, } from "./ICookie.js";
2
+ export type { ISession, ISessionStorage, ISerializedSession, SessionIntent, SessionStatusType, SessionStrategyType, FlashBagType, MetaBagType, } from "./ISession.js";
3
+ export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, } from "./IUpload.js";
4
+ export type { IRequest, IHttpRequest, IHttp2Request, IWsRequest, HTTPMethodType, } from "./IRequest.js";
5
+ export type { IResponse, IHttpResponse, IWebsocketResponse } from "./IResponse.js";
6
+ export type { IContext, IHttpContext, IWebsocketContext, ServerType, SchemeType, WebSocketStateType, CookiesMap, } from "./IContext.js";
7
+ export type { IHttpKernel } from "./IHttpKernel.js";
@@ -0,0 +1,18 @@
1
+ import type { Module } from "nodefony";
2
+ import type { IAdminApi } from "nodefony";
3
+ /**
4
+ * Producteur `IAdminApi` du module **http** — exposé sous `/nodefony/http/api/*`.
5
+ *
6
+ * 2ᵉ producteur du data plane admin (le 1er étant le kernel). Démontre le
7
+ * pattern multi-modules : `@nodefony/http` n'importe QUE le contrat core
8
+ * (`IAdminApi`) — jamais `@nodefony/framework` (dépendance circulaire). Il
9
+ * s'enregistre auprès du broker via `IAdminRegistry` récupéré du container.
10
+ *
11
+ * Endpoints :
12
+ * - `GET /nodefony/http/api/servers` → liste des serveurs réseau + leur état
13
+ * - `GET /nodefony/http/api/info` → résumé (serveurs prêts, ports, schemes)
14
+ *
15
+ * @param module - le module http (accès aux services serveur du container).
16
+ * @returns le contrat admin de http, prêt à `registry.register()`.
17
+ */
18
+ export declare function createHttpAdminApi(module: Module): IAdminApi;
@@ -0,0 +1,23 @@
1
+ import type { IAdminApi } from "nodefony";
2
+ import type { Profiler } from "../src/profiler/Profiler.js";
3
+ /**
4
+ * Producteur `IAdminApi` du **profiler** — exposé sous `/nodefony/profiler/api/*`.
5
+ *
6
+ * Namespace dédié (≠ replié dans `http`) car le profiling par requête est un
7
+ * concern transverse : timing par phase, route, user, futur SQL/audit. Il a sa
8
+ * propre entrée Studio et n'est monté qu'en **dev** (le module n'instancie le
9
+ * {@link Profiler} qu'hors prod).
10
+ *
11
+ * Endpoints :
12
+ * - `GET /nodefony/profiler/api/recent` → derniers profils (résumés, récent → ancien)
13
+ * - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId
14
+ * - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
15
+ *
16
+ * La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
17
+ * fetch `/{id}` — corrélation client↔serveur gratuite.
18
+ *
19
+ * @param profiler - l'instance partagée du ring buffer (même que le hook kernel).
20
+ * @returns le contrat admin du profiler, prêt à `registry.register()`.
21
+ */
22
+ export declare function createProfilerAdminApi(profiler: Profiler): IAdminApi;
23
+ export default createProfilerAdminApi;
@@ -0,0 +1,143 @@
1
+ import type { Severity } from "nodefony";
2
+ import type { IRequestLogger, IRequestLogEntry } from "../interfaces/IRequestLogger.js";
3
+ import type { IHttpContext, IWebsocketContext } from "../interfaces/IContext.js";
4
+ /**
5
+ * Canonical audit log entry — 1 JSON PDU per request (P3.1).
6
+ *
7
+ * Fed to `context.log()` as a stringified JSON payload. Ingest pipelines
8
+ * (Vision, Loki, ELK, OpenTelemetry...) can parse it directly without
9
+ * regex on a colored line.
10
+ *
11
+ * Includes P3.3 (severity per HTTP status) for free and exposes phase
12
+ * timings (P1.1) for downstream trace tools (P3.7).
13
+ *
14
+ * Header redaction (P3.4) — Authorization / Cookie / Set-Cookie are
15
+ * never serialised here. We log presence-only flags instead.
16
+ */
17
+ export interface AuditLogEntry {
18
+ ts: string;
19
+ requestId: string;
20
+ userId: string | null;
21
+ type: "http" | "ws";
22
+ scheme: string;
23
+ method: string | null;
24
+ url: string;
25
+ status: number | null;
26
+ durationMs: number | null;
27
+ remoteAddress: string | null;
28
+ host: string | null;
29
+ userAgent: string | null;
30
+ hasAuthorization: boolean;
31
+ hasCookie: boolean;
32
+ phases?: {
33
+ name: string;
34
+ durationMs: number | null;
35
+ }[];
36
+ error?: AuditErrorEntry;
37
+ protocol?: string | null;
38
+ }
39
+ /**
40
+ * Enriched error description for audit logs (P3.5).
41
+ * Includes optional cause chain (Error.cause) and stack (dev only).
42
+ */
43
+ export interface AuditErrorEntry {
44
+ name: string;
45
+ message: string;
46
+ code?: number;
47
+ /** nodefonyError's domain classifier when available (P1.5 / Phase 1). */
48
+ errorType?: string;
49
+ /** Multi-line stack — dev/development only, omitted in prod for safety. */
50
+ stack?: string;
51
+ /** Recursive — capped to depth 5 to avoid pathological cycles. */
52
+ cause?: AuditErrorEntry;
53
+ }
54
+ /**
55
+ * Severity derived from HTTP status code — RFC 9110 categories.
56
+ * 1xx/2xx/3xx → INFO ; 4xx → WARNING ; 5xx → ERROR.
57
+ * Unknown/missing status → INFO.
58
+ */
59
+ declare function severityFromStatus(status: number | null | undefined): Severity;
60
+ export interface JsonAuditLoggerOptions {
61
+ /**
62
+ * Whether to include `error.stack` and recursive `error.cause.stack`.
63
+ * Default `process.env.NODE_ENV !== "production"` — auto-hidden in prod.
64
+ * Override explicitly for stricter security or to enable in staging.
65
+ */
66
+ includeStack?: boolean;
67
+ /**
68
+ * Max depth for `Error.cause` chain serialisation.
69
+ * Default `5`. Prevents pathological cycles and oversized log entries.
70
+ */
71
+ maxCauseDepth?: number;
72
+ /**
73
+ * Sampling rate for nominal (2xx/3xx) audit logs — perf lever on the hot
74
+ * path (L3). `1` (default) logs every request. `N > 1` logs only 1 in N of
75
+ * the 2xx/3xx requests, **but always logs `status >= 400` and errored
76
+ * requests** (you never lose a failure). Counted with a deterministic
77
+ * counter — no RNG (consistent with the L2 entropy amortisation).
78
+ *
79
+ * Skipped requests never reach `renderHttp`, so they cost **zero** object
80
+ * allocation and zero `JSON.stringify`. Configure via
81
+ * `kernel.options.log.requestLogger.sampleRate`.
82
+ */
83
+ sampleRate?: number;
84
+ /**
85
+ * T1 (profil delta vs Express) — audit du chemin NOMINAL (2xx/3xx).
86
+ * `false` → seules les requêtes en erreur et les `status >= 400` sont
87
+ * auditées (jamais gâtées — OWASP, faible volume, valeur forensique max).
88
+ * Résolu AU BOOT par `HttpKernel.applyRequestLoggerFromConfig` depuis
89
+ * `log.requestLogger.nominal` (`"auto"` défaut = coupé SSI
90
+ * `log.driver === "null"`, où l'entrée d'audit n'atteint AUCUNE destination
91
+ * texte — elle coûtait objet + toISOString + stringify + Pdu ring pour rien,
92
+ * ~5,9 % du profil CPU). Défaut `true` (comportement historique).
93
+ */
94
+ nominal?: boolean;
95
+ }
96
+ /**
97
+ * JSON audit logger — implements IRequestLogger so it slots into
98
+ * `httpKernel.setRequestLogger(new JsonAuditLogger())`.
99
+ *
100
+ * Stateless singleton. Allocates one plain object + one JSON.stringify per
101
+ * request — acceptable since this is the terminal log path (1 per req).
102
+ */
103
+ declare class JsonAuditLogger implements IRequestLogger {
104
+ private readonly includeStack;
105
+ private readonly maxCauseDepth;
106
+ /** Sampling divisor for 2xx/3xx logs (`1` = log all). Always ≥ 1. */
107
+ private readonly sampleRate;
108
+ /** Deterministic 0-based counter for `1/sampleRate` selection (no RNG). */
109
+ private sampleCounter;
110
+ /** T1 — `false` = audit nominal coupé (erreurs/4xx/5xx toujours audités). */
111
+ private readonly nominalEnabled;
112
+ constructor(opts?: JsonAuditLoggerOptions);
113
+ /**
114
+ * Decide whether the current HTTP request must be logged (audit sampling).
115
+ *
116
+ * Always `true` when `sampleRate <= 1`, on errors, and for `status >= 400`
117
+ * (failures are never sampled out). Otherwise selects 1 in `sampleRate` of
118
+ * the 2xx/3xx requests with a deterministic counter.
119
+ *
120
+ * Called by `Context.logRequest()` **before** `renderHttp`, so a sampled-out
121
+ * request allocates nothing and runs no `JSON.stringify`.
122
+ *
123
+ * @param context - the HTTP context being finalised
124
+ * @param error - error captured for this request, if any
125
+ * @returns `true` to render+log the entry, `false` to skip it
126
+ */
127
+ shouldSample(context: IHttpContext, error?: Error | null): boolean;
128
+ renderHttp(context: IHttpContext, error?: Error | null): IRequestLogEntry;
129
+ renderWebsocket(context: IWebsocketContext, error?: Error | null, acceptedProtocol?: string | null): IRequestLogEntry;
130
+ /**
131
+ * Total request duration computed from the first phase startMs to now.
132
+ * Returns null if timing is disabled (no phases recorded).
133
+ */
134
+ private computeDurationMs;
135
+ /**
136
+ * Serialise an Error (recursively for `cause` chain) into an AuditErrorEntry.
137
+ * Stack is included only when `includeStack === true` (dev default).
138
+ * Cause chain is capped at `maxCauseDepth`.
139
+ */
140
+ private serializeError;
141
+ }
142
+ export default JsonAuditLogger;
143
+ export { severityFromStatus };