@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,376 @@
1
+ import { SESSION_FACETS, SESSION_FILTERS, SESSION_STATS_FILTERS } from "../src/session/storage/sessionFilters.js";
2
+ import { RATE_LIMIT_FILTERS } from "../src/rateLimit/rateLimitFilters.js";
3
+ import { parseFilters, parsePageQuery } from "nodefony";
4
+ //#region nodefony/service/HttpAdminApi.ts
5
+ /** Nom du service HttpKernel dans le container (source unique). */
6
+ const HTTP_KERNEL_SERVICE = "HttpKernel";
7
+ /**
8
+ * `limit`/`offset` du contrat de page, avec l'`offset` **matérialisé** : ces
9
+ * endpoints le renvoient dans leur réponse, où l'absence n'a pas de sens (le
10
+ * client lit « page 1 », pas « pas de décalage »). Le traducteur, lui, laisse
11
+ * `offset` absent quand le client n'en demande pas — c'est le contrat.
12
+ */
13
+ function pageParams(query) {
14
+ const parsed = parsePageQuery(query, { searchable: true });
15
+ return {
16
+ limit: parsed.limit,
17
+ offset: parsed.offset ?? 0,
18
+ ...parsed.q !== void 0 ? { q: parsed.q } : {}
19
+ };
20
+ }
21
+ /**
22
+ * Libellé d'identité de l'admin appelant (pour l'audit) — duck-typing prudent
23
+ * sur l'`IUser` projeté dans `IAdminRequest.user`. Repli `"admin"`.
24
+ */
25
+ function adminActor(user) {
26
+ if (user && typeof user === "object") {
27
+ const u = user;
28
+ if (typeof u.username === "string" && u.username) return u.username;
29
+ if (typeof u.identifier === "string" && u.identifier) return u.identifier;
30
+ }
31
+ return "admin";
32
+ }
33
+ /**
34
+ * Identifiant de l'appelant authentifié pour le SCOPE self-service — lu sur l'IUser
35
+ * projeté dans `IAdminRequest.user` (= `session.user`, posé au login). `null` si
36
+ * absent/vide → le handler répond 401 (jamais de scope vide qui listerait les
37
+ * sessions anonymes). **Jamais** dérivé d'un paramètre client (anti-IDOR).
38
+ */
39
+ function currentIdentifier(user) {
40
+ if (user && typeof user === "object") {
41
+ const u = user;
42
+ if (typeof u.identifier === "string" && u.identifier.length > 0) return u.identifier;
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Producteur `IAdminApi` du module **http** — exposé sous `/nodefony/http/api/*`.
48
+ *
49
+ * 2ᵉ producteur du data plane admin (le 1er étant le kernel). Démontre le
50
+ * pattern multi-modules : `@nodefony/http` n'importe QUE le contrat core
51
+ * (`IAdminApi`) — jamais `@nodefony/framework` (dépendance circulaire). Il
52
+ * s'enregistre auprès du broker via `IAdminRegistry` récupéré du container.
53
+ *
54
+ * Endpoints :
55
+ * - `GET /nodefony/http/api/servers` → liste des serveurs réseau + leur état
56
+ * - `GET /nodefony/http/api/info` → résumé (serveurs prêts, ports, schemes)
57
+ *
58
+ * @param module - le module http (accès aux services serveur du container).
59
+ * @returns le contrat admin de http, prêt à `registry.register()`.
60
+ */
61
+ function createHttpAdminApi(module) {
62
+ /** Noms des services serveur enregistrés par le module http. */
63
+ const serverServices = [
64
+ "server-http",
65
+ "server-https",
66
+ "server-websocket",
67
+ "server-websocket-secure",
68
+ "server-static"
69
+ ];
70
+ const readServer = (name) => {
71
+ const svc = module.get(name);
72
+ if (!svc) return null;
73
+ return {
74
+ service: name,
75
+ type: svc.type,
76
+ scheme: svc.scheme,
77
+ protocol: svc.protocol,
78
+ address: svc.address,
79
+ port: svc.port,
80
+ family: svc.family ?? null,
81
+ ready: svc.ready ?? false
82
+ };
83
+ };
84
+ const listServers = () => serverServices.map((name) => readServer(name)).filter((s) => s !== null);
85
+ const descriptor = {
86
+ label: "HTTP",
87
+ icon: "network",
88
+ order: 1
89
+ };
90
+ const endpoints = [
91
+ {
92
+ path: "servers",
93
+ summary: "Network servers (http/https/ws/wss/static) with listening state",
94
+ handler: () => listServers()
95
+ },
96
+ {
97
+ path: "info",
98
+ summary: "HTTP layer summary — ready servers, ports, schemes",
99
+ handler: () => {
100
+ const servers = listServers();
101
+ const ready = servers.filter((s) => s.ready);
102
+ return {
103
+ serversTotal: servers.length,
104
+ serversReady: ready.length,
105
+ ports: [...new Set(ready.map((s) => s.port).filter(Boolean))],
106
+ schemes: [...new Set(ready.map((s) => s.scheme).filter(Boolean))],
107
+ protocols: [...new Set(ready.map((s) => s.protocol).filter(Boolean))]
108
+ };
109
+ }
110
+ },
111
+ {
112
+ path: "rate-limit/list",
113
+ method: "GET",
114
+ role: "ROLE_NODEFONY_ADMIN",
115
+ summary: "Clés (IP) suivies par le rate-limit général, les plus bruyantes d'abord. Paginé serveur : ?limited&q&limit&offset. `enabled:false` = rate-limit désarmé en config (liste vide, pas une erreur).",
116
+ handler: async (request) => {
117
+ const { limit, offset, q } = pageParams(request.query);
118
+ const store = module.get(HTTP_KERNEL_SERVICE)?.rateLimitStore;
119
+ if (!store) return {
120
+ enabled: false,
121
+ trackedCount: 0,
122
+ rejectedTotal: 0,
123
+ items: [],
124
+ total: 0,
125
+ limit,
126
+ offset
127
+ };
128
+ const page = await store.listPage({
129
+ limit,
130
+ offset,
131
+ ...parseFilters(request.query, RATE_LIMIT_FILTERS),
132
+ ...q !== void 0 ? { q } : {}
133
+ });
134
+ return {
135
+ enabled: true,
136
+ trackedCount: store.trackedCount,
137
+ rejectedTotal: store.rejectedTotal,
138
+ items: page.items,
139
+ total: page.total,
140
+ limit,
141
+ offset
142
+ };
143
+ }
144
+ },
145
+ {
146
+ path: "sessions",
147
+ summary: "Session subsystem status + active count (web BFF auth)",
148
+ handler: async () => {
149
+ const svc = module.get("sessions");
150
+ if (!svc) return {
151
+ enabled: false,
152
+ active: 0
153
+ };
154
+ const inner = svc.storage?.inner ?? null;
155
+ const storage = inner?.constructor?.name ?? svc.storage?.constructor?.name ?? "none";
156
+ const driver = svc.options?.store ?? null;
157
+ const revocationHardened = inner !== null;
158
+ return {
159
+ enabled: true,
160
+ strategy: svc.sessionStrategy ?? null,
161
+ activation: "intent",
162
+ name: svc.defaultSessionName ?? null,
163
+ driver,
164
+ storage,
165
+ revocationHardened,
166
+ idleTimeoutS: svc.options?.idleTimeoutS ?? null,
167
+ absoluteTimeoutS: svc.options?.absoluteTimeoutS ?? null,
168
+ savePath: null,
169
+ active: null
170
+ };
171
+ }
172
+ },
173
+ {
174
+ path: "sessions/list",
175
+ method: "GET",
176
+ role: "ROLE_NODEFONY_ADMIN",
177
+ summary: "Sessions actives (ref/user/ip/ua/dates — jamais l'id de session). Paginé côté serveur : ?user&limit&offset. `total` absent et `nextCursor` présent sur un backend à curseur (Redis).",
178
+ page: {
179
+ sortable: () => {
180
+ const svc = module.get("sessions");
181
+ return svc?.supportsEnumeration() ? svc.sortableFields() : [];
182
+ },
183
+ filters: SESSION_FILTERS
184
+ },
185
+ handler: async (request) => {
186
+ const svc = module.get("sessions");
187
+ if (!svc) return {
188
+ status: 503,
189
+ body: { error: "session service unavailable" }
190
+ };
191
+ if (!svc.supportsEnumeration()) return {
192
+ status: 501,
193
+ body: { error: "session enumeration not supported by storage" }
194
+ };
195
+ const pageQuery = parsePageQuery(request.query, { sortable: svc.sortableFields() });
196
+ const { limit } = pageQuery;
197
+ const offset = pageQuery.offset ?? 0;
198
+ const page = await svc.listSessionsPage({
199
+ limit,
200
+ offset,
201
+ ...pageQuery.cursor ? { cursor: pageQuery.cursor } : {},
202
+ ...parseFilters(request.query, SESSION_FILTERS),
203
+ ...pageQuery.order ? { order: pageQuery.order } : {}
204
+ });
205
+ return {
206
+ items: page.items,
207
+ total: page.total,
208
+ limit,
209
+ offset,
210
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
211
+ };
212
+ }
213
+ },
214
+ {
215
+ path: "sessions/stats",
216
+ method: "GET",
217
+ role: "ROLE_NODEFONY_ADMIN",
218
+ summary: "Compteurs des sessions sur la collection ENTIÈRE (total, authentifiées, anonymes, utilisateurs distincts) — mêmes filtres que sessions/list. Un compteur `null` = le backend ne sait pas le calculer (Redis).",
219
+ page: {
220
+ filters: SESSION_STATS_FILTERS,
221
+ facets: SESSION_FACETS
222
+ },
223
+ handler: async (request) => {
224
+ const svc = module.get("sessions");
225
+ if (!svc) return {
226
+ status: 503,
227
+ body: { error: "session service unavailable" }
228
+ };
229
+ if (!svc.supportsEnumeration()) return {
230
+ status: 501,
231
+ body: { error: "session enumeration not supported by storage" }
232
+ };
233
+ parsePageQuery(request.query, {});
234
+ return svc.countSessionFacets(parseFilters(request.query, SESSION_STATS_FILTERS));
235
+ }
236
+ },
237
+ {
238
+ path: "sessions/{ref}/revoke",
239
+ method: "POST",
240
+ role: "ROLE_NODEFONY_ADMIN",
241
+ summary: "Révoque une session par sa référence publique (sess_…). Audité. 404 si la référence ne correspond à aucune session.",
242
+ handler: async (request) => {
243
+ const svc = module.get("sessions");
244
+ if (!svc) return {
245
+ status: 503,
246
+ body: { error: "session service unavailable" }
247
+ };
248
+ if (!svc.supportsEnumeration()) return {
249
+ status: 501,
250
+ body: { error: "session enumeration not supported by storage" }
251
+ };
252
+ const ref = request.params.ref;
253
+ if (typeof ref !== "string" || ref.length === 0) return {
254
+ status: 404,
255
+ body: { error: "not found" }
256
+ };
257
+ if (!await svc.destroyByRef(ref, adminActor(request.user))) return {
258
+ status: 404,
259
+ body: { error: "not found" }
260
+ };
261
+ return { ok: true };
262
+ }
263
+ },
264
+ {
265
+ path: "sessions/revoke-user/{identifier}",
266
+ method: "POST",
267
+ role: "ROLE_NODEFONY_ADMIN",
268
+ summary: "Déconnecte TOUTES les sessions d'un utilisateur (logout everywhere). Audité. Renvoie le nombre de sessions détruites.",
269
+ handler: async (request) => {
270
+ const svc = module.get("sessions");
271
+ if (!svc) return {
272
+ status: 503,
273
+ body: { error: "session service unavailable" }
274
+ };
275
+ if (!svc.supportsEnumeration()) return {
276
+ status: 501,
277
+ body: { error: "session enumeration not supported by storage" }
278
+ };
279
+ const identifier = request.params.identifier;
280
+ if (typeof identifier !== "string" || identifier.length === 0) return {
281
+ status: 400,
282
+ body: { error: "identifier required" }
283
+ };
284
+ return {
285
+ ok: true,
286
+ count: await svc.destroyByUser(identifier, adminActor(request.user))
287
+ };
288
+ }
289
+ },
290
+ {
291
+ path: "sessions/mine",
292
+ method: "GET",
293
+ public: true,
294
+ summary: "MES sessions (self-service) — ref/ip/ua/dates, scopées à l'appelant. Paginé côté serveur : ?limit&offset.",
295
+ page: {
296
+ sortable: () => {
297
+ const svc = module.get("sessions");
298
+ return svc?.supportsEnumeration() ? svc.sortableFields() : [];
299
+ },
300
+ filters: {}
301
+ },
302
+ handler: async (request) => {
303
+ const svc = module.get("sessions");
304
+ if (!svc) return {
305
+ status: 503,
306
+ body: { error: "session service unavailable" }
307
+ };
308
+ if (!svc.supportsEnumeration()) return {
309
+ status: 501,
310
+ body: { error: "session enumeration not supported by storage" }
311
+ };
312
+ const identifier = currentIdentifier(request.user);
313
+ if (!identifier) return {
314
+ status: 401,
315
+ body: { error: "unauthenticated" }
316
+ };
317
+ const ownQuery = parsePageQuery(request.query, { sortable: svc.sortableFields() });
318
+ parseFilters(request.query, {});
319
+ const { limit } = ownQuery;
320
+ const offset = ownQuery.offset ?? 0;
321
+ const page = await svc.listOwnSessionsPage(identifier, {
322
+ limit,
323
+ offset,
324
+ ...ownQuery.cursor ? { cursor: ownQuery.cursor } : {},
325
+ ...ownQuery.order ? { order: ownQuery.order } : {}
326
+ });
327
+ return {
328
+ items: page.items,
329
+ total: page.total,
330
+ limit,
331
+ offset,
332
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
333
+ };
334
+ }
335
+ },
336
+ {
337
+ path: "sessions/mine/{ref}/revoke",
338
+ method: "POST",
339
+ public: true,
340
+ summary: "Révoque UNE de MES sessions par sa référence (sess_…). 404 si la référence n'est pas une de mes sessions. Audité.",
341
+ handler: async (request) => {
342
+ const svc = module.get("sessions");
343
+ if (!svc) return {
344
+ status: 503,
345
+ body: { error: "session service unavailable" }
346
+ };
347
+ if (!svc.supportsEnumeration()) return {
348
+ status: 501,
349
+ body: { error: "session enumeration not supported by storage" }
350
+ };
351
+ const identifier = currentIdentifier(request.user);
352
+ if (!identifier) return {
353
+ status: 401,
354
+ body: { error: "unauthenticated" }
355
+ };
356
+ const ref = request.params.ref;
357
+ if (typeof ref !== "string" || ref.length === 0) return {
358
+ status: 404,
359
+ body: { error: "not found" }
360
+ };
361
+ if (!await svc.destroyOwnByRef(identifier, ref, identifier)) return {
362
+ status: 404,
363
+ body: { error: "not found" }
364
+ };
365
+ return { ok: true };
366
+ }
367
+ }
368
+ ];
369
+ return {
370
+ adminNamespace: "http",
371
+ adminDescriptor: () => descriptor,
372
+ adminEndpoints: () => endpoints
373
+ };
374
+ }
375
+ //#endregion
376
+ export { createHttpAdminApi };
@@ -0,0 +1,73 @@
1
+ //#region nodefony/service/ProfilerAdminApi.ts
2
+ /**
3
+ * Producteur `IAdminApi` du **profiler** — exposé sous `/nodefony/profiler/api/*`.
4
+ *
5
+ * Namespace dédié (≠ replié dans `http`) car le profiling par requête est un
6
+ * concern transverse : timing par phase, route, user, futur SQL/audit. Il a sa
7
+ * propre entrée Studio et n'est monté qu'en **dev** (le module n'instancie le
8
+ * {@link Profiler} qu'hors prod).
9
+ *
10
+ * Endpoints :
11
+ * - `GET /nodefony/profiler/api/recent` → derniers profils (résumés, récent → ancien)
12
+ * - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId
13
+ * - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
14
+ *
15
+ * La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
16
+ * fetch `/{id}` — corrélation client↔serveur gratuite.
17
+ *
18
+ * @param profiler - l'instance partagée du ring buffer (même que le hook kernel).
19
+ * @returns le contrat admin du profiler, prêt à `registry.register()`.
20
+ */
21
+ function createProfilerAdminApi(profiler) {
22
+ const descriptor = {
23
+ label: "Profiler",
24
+ icon: "bug",
25
+ order: 9
26
+ };
27
+ const endpoints = [
28
+ {
29
+ path: "recent",
30
+ summary: "Recent request profiles (summaries, newest first)",
31
+ handler: ({ query }) => {
32
+ const raw = query.limit;
33
+ const n = typeof raw === "string" ? parseInt(raw, 10) : NaN;
34
+ const limit = Number.isFinite(n) && n > 0 ? Math.min(n, 200) : 60;
35
+ return {
36
+ count: profiler.size,
37
+ entries: profiler.recent(limit)
38
+ };
39
+ }
40
+ },
41
+ {
42
+ path: "recent",
43
+ method: "DELETE",
44
+ summary: "Clear the profiler ring buffer",
45
+ handler: () => {
46
+ profiler.clear();
47
+ return { cleared: true };
48
+ }
49
+ },
50
+ {
51
+ path: "{id}",
52
+ summary: "Full profile (phase timeline) for a requestId",
53
+ handler: ({ params }) => {
54
+ const entry = profiler.get(params.id);
55
+ if (!entry) return {
56
+ status: 404,
57
+ body: {
58
+ error: "Profile not found",
59
+ requestId: params.id
60
+ }
61
+ };
62
+ return entry;
63
+ }
64
+ }
65
+ ];
66
+ return {
67
+ adminNamespace: "profiler",
68
+ adminDescriptor: () => descriptor,
69
+ adminEndpoints: () => endpoints
70
+ };
71
+ }
72
+ //#endregion
73
+ export { createProfilerAdminApi, createProfilerAdminApi as default };
@@ -0,0 +1,159 @@
1
+ import { RequestContext } from "nodefony";
2
+ import { performance } from "node:perf_hooks";
3
+ //#region nodefony/service/audit-logger.ts
4
+ /**
5
+ * Severity derived from HTTP status code — RFC 9110 categories.
6
+ * 1xx/2xx/3xx → INFO ; 4xx → WARNING ; 5xx → ERROR.
7
+ * Unknown/missing status → INFO.
8
+ */
9
+ function severityFromStatus(status) {
10
+ if (!status) return "INFO";
11
+ if (status >= 500) return "ERROR";
12
+ if (status >= 400) return "WARNING";
13
+ return "INFO";
14
+ }
15
+ /**
16
+ * JSON audit logger — implements IRequestLogger so it slots into
17
+ * `httpKernel.setRequestLogger(new JsonAuditLogger())`.
18
+ *
19
+ * Stateless singleton. Allocates one plain object + one JSON.stringify per
20
+ * request — acceptable since this is the terminal log path (1 per req).
21
+ */
22
+ var JsonAuditLogger = class {
23
+ includeStack;
24
+ maxCauseDepth;
25
+ /** Sampling divisor for 2xx/3xx logs (`1` = log all). Always ≥ 1. */
26
+ sampleRate;
27
+ /** Deterministic 0-based counter for `1/sampleRate` selection (no RNG). */
28
+ sampleCounter = 0;
29
+ /** T1 — `false` = audit nominal coupé (erreurs/4xx/5xx toujours audités). */
30
+ nominalEnabled;
31
+ constructor(opts = {}) {
32
+ this.includeStack = opts.includeStack ?? process.env.NODE_ENV !== "production";
33
+ this.maxCauseDepth = opts.maxCauseDepth ?? 5;
34
+ const rate = opts.sampleRate ?? 1;
35
+ this.sampleRate = Number.isFinite(rate) && rate >= 1 ? Math.floor(rate) : 1;
36
+ this.nominalEnabled = opts.nominal ?? true;
37
+ }
38
+ /**
39
+ * Decide whether the current HTTP request must be logged (audit sampling).
40
+ *
41
+ * Always `true` when `sampleRate <= 1`, on errors, and for `status >= 400`
42
+ * (failures are never sampled out). Otherwise selects 1 in `sampleRate` of
43
+ * the 2xx/3xx requests with a deterministic counter.
44
+ *
45
+ * Called by `Context.logRequest()` **before** `renderHttp`, so a sampled-out
46
+ * request allocates nothing and runs no `JSON.stringify`.
47
+ *
48
+ * @param context - the HTTP context being finalised
49
+ * @param error - error captured for this request, if any
50
+ * @returns `true` to render+log the entry, `false` to skip it
51
+ */
52
+ shouldSample(context, error) {
53
+ const ctx = context;
54
+ if (!this.nominalEnabled) {
55
+ if (error ?? ctx.error) return true;
56
+ const status = ctx.response?.statusCode ?? null;
57
+ return status !== null && status >= 400;
58
+ }
59
+ if (this.sampleRate <= 1) return true;
60
+ if (error ?? ctx.error) return true;
61
+ const status = ctx.response?.statusCode ?? null;
62
+ if (status !== null && status >= 400) return true;
63
+ this.sampleCounter = (this.sampleCounter + 1) % this.sampleRate;
64
+ return this.sampleCounter === 0;
65
+ }
66
+ renderHttp(context, error) {
67
+ const ctx = context;
68
+ const status = ctx.response?.statusCode ?? null;
69
+ const headers = ctx.request?.headers ?? {};
70
+ const err = error ?? ctx.error ?? null;
71
+ const entry = {
72
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
73
+ requestId: ctx.requestId,
74
+ userId: RequestContext.getUserId() ?? null,
75
+ type: "http",
76
+ scheme: ctx.scheme,
77
+ method: ctx.method,
78
+ url: ctx.url,
79
+ status,
80
+ durationMs: this.computeDurationMs(ctx.phases),
81
+ remoteAddress: ctx.remoteAddress ?? null,
82
+ host: ctx.getHost?.() ?? null,
83
+ userAgent: ctx.getUserAgent?.() ?? null,
84
+ hasAuthorization: Boolean(headers["authorization"]),
85
+ hasCookie: Boolean(headers["cookie"]),
86
+ phases: ctx.phases.length ? ctx.phases.map((p) => ({
87
+ name: p.name,
88
+ durationMs: p.durationMs ?? null
89
+ })) : void 0,
90
+ error: err ? this.serializeError(err, 0) : void 0
91
+ };
92
+ return {
93
+ text: JSON.stringify(entry),
94
+ severity: severityFromStatus(status ?? (err ? 500 : null)),
95
+ msgid: "audit"
96
+ };
97
+ }
98
+ renderWebsocket(context, error, acceptedProtocol) {
99
+ const ctx = context;
100
+ const status = ctx.response?.statusCode ?? null;
101
+ const headers = ctx.request?.headers ?? {};
102
+ const entry = {
103
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
104
+ requestId: ctx.requestId,
105
+ userId: RequestContext.getUserId() ?? null,
106
+ type: "ws",
107
+ scheme: ctx.scheme,
108
+ method: ctx.method,
109
+ url: ctx.url,
110
+ status,
111
+ durationMs: this.computeDurationMs(ctx.phases),
112
+ remoteAddress: ctx.remoteAddress ?? null,
113
+ host: ctx.getHost?.() ?? null,
114
+ userAgent: ctx.getUserAgent?.() ?? null,
115
+ hasAuthorization: Boolean(headers["authorization"]),
116
+ hasCookie: Boolean(headers["cookie"]),
117
+ phases: ctx.phases.length ? ctx.phases.map((p) => ({
118
+ name: p.name,
119
+ durationMs: p.durationMs ?? null
120
+ })) : void 0,
121
+ protocol: acceptedProtocol ?? null,
122
+ error: error ? this.serializeError(error, 0) : void 0
123
+ };
124
+ return {
125
+ text: JSON.stringify(entry),
126
+ severity: error ? "ERROR" : "INFO",
127
+ msgid: "audit"
128
+ };
129
+ }
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
+ computeDurationMs(phases) {
135
+ if (!phases.length) return null;
136
+ const first = phases[0];
137
+ if (typeof first.startMs !== "number") return null;
138
+ return performance.now() - first.startMs;
139
+ }
140
+ /**
141
+ * Serialise an Error (recursively for `cause` chain) into an AuditErrorEntry.
142
+ * Stack is included only when `includeStack === true` (dev default).
143
+ * Cause chain is capped at `maxCauseDepth`.
144
+ */
145
+ serializeError(err, depth) {
146
+ const e = err;
147
+ const entry = {
148
+ name: e.name ?? "Error",
149
+ message: e.message ?? String(err)
150
+ };
151
+ if (typeof e.code === "number") entry.code = e.code;
152
+ if (typeof e.errorType === "string") entry.errorType = e.errorType;
153
+ if (this.includeStack && typeof e.stack === "string") entry.stack = e.stack;
154
+ if (e.cause && depth + 1 < this.maxCauseDepth) entry.cause = this.serializeError(e.cause, depth + 1);
155
+ return entry;
156
+ }
157
+ };
158
+ //#endregion
159
+ export { JsonAuditLogger as default, severityFromStatus };