@nodefony/security 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 (258) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +182 -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/index.js +151 -0
  6. package/dist/nodefony/command/security-secrets.js +158 -0
  7. package/dist/nodefony/command/security-token.js +335 -0
  8. package/dist/nodefony/command/security-user-add.js +131 -0
  9. package/dist/nodefony/command/security-user-delete.js +102 -0
  10. package/dist/nodefony/command/security-user-list.js +77 -0
  11. package/dist/nodefony/config/config.js +366 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +35 -0
  13. package/dist/nodefony/contracts/IAccessVoter.js +13 -0
  14. package/dist/nodefony/contracts/IApiKey.js +1 -0
  15. package/dist/nodefony/contracts/IAuditEvent.js +1 -0
  16. package/dist/nodefony/contracts/IAuditStore.js +1 -0
  17. package/dist/nodefony/contracts/IAuthenticator.js +1 -0
  18. package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
  19. package/dist/nodefony/contracts/IFirewall.js +1 -0
  20. package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
  21. package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
  22. package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
  23. package/dist/nodefony/contracts/ISecuredArea.js +1 -0
  24. package/dist/nodefony/contracts/IToken.js +1 -0
  25. package/dist/nodefony/contracts/ITokenStore.js +1 -0
  26. package/dist/nodefony/contracts/ITotpSecret.js +1 -0
  27. package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
  28. package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
  29. package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
  30. package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
  31. package/dist/nodefony/contracts/IWebhookStore.js +1 -0
  32. package/dist/nodefony/contracts/index.js +2 -0
  33. package/dist/nodefony/errors/AccessDeniedError.js +14 -0
  34. package/dist/nodefony/errors/ApiKeyError.js +21 -0
  35. package/dist/nodefony/errors/AuthenticationError.js +14 -0
  36. package/dist/nodefony/errors/CsrfError.js +23 -0
  37. package/dist/nodefony/errors/InvalidTargetError.js +39 -0
  38. package/dist/nodefony/errors/SsrfError.js +17 -0
  39. package/dist/nodefony/errors/ThrottledError.js +21 -0
  40. package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
  41. package/dist/nodefony/errors/WebAuthnError.js +21 -0
  42. package/dist/nodefony/errors/index.js +9 -0
  43. package/dist/nodefony/service/accessTokenVerifier.js +77 -0
  44. package/dist/nodefony/service/apiKeys.js +310 -0
  45. package/dist/nodefony/service/auditService.js +145 -0
  46. package/dist/nodefony/service/authFlow.js +332 -0
  47. package/dist/nodefony/service/authorization.js +95 -0
  48. package/dist/nodefony/service/cors.js +81 -0
  49. package/dist/nodefony/service/csrf.js +97 -0
  50. package/dist/nodefony/service/firewall.js +699 -0
  51. package/dist/nodefony/service/oauth2.js +153 -0
  52. package/dist/nodefony/service/securityHeaders.js +80 -0
  53. package/dist/nodefony/service/tokenService.js +486 -0
  54. package/dist/nodefony/service/totp.js +209 -0
  55. package/dist/nodefony/service/webAuthn.js +343 -0
  56. package/dist/nodefony/service/webhooks.js +539 -0
  57. package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
  58. package/dist/nodefony/src/SecuredArea.js +51 -0
  59. package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
  60. package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
  61. package/dist/nodefony/src/admin/adminAudit.js +37 -0
  62. package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
  63. package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
  64. package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
  65. package/dist/nodefony/src/audit/auditBridge.js +82 -0
  66. package/dist/nodefony/src/audit/auditFilters.js +60 -0
  67. package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
  68. package/dist/nodefony/src/audit/readAuditContext.js +24 -0
  69. package/dist/nodefony/src/audit/recordAudit.js +16 -0
  70. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
  71. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
  72. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
  73. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
  74. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
  75. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
  76. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
  77. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
  78. package/dist/nodefony/src/authenticator/bearer.js +2 -0
  79. package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
  80. package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
  81. package/dist/nodefony/src/crypto/secretCipher.js +79 -0
  82. package/dist/nodefony/src/csp.js +54 -0
  83. package/dist/nodefony/src/csrfToken.js +65 -0
  84. package/dist/nodefony/src/net/ssrfGuard.js +130 -0
  85. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +65 -0
  87. package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
  88. package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
  89. package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
  90. package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
  91. package/dist/nodefony/src/sessionIdentity.js +35 -0
  92. package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
  93. package/dist/nodefony/src/token/AnonymousToken.js +40 -0
  94. package/dist/nodefony/src/token/JwtKeystore.js +160 -0
  95. package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
  96. package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
  97. package/dist/nodefony/src/token/UserToken.js +67 -0
  98. package/dist/nodefony/src/token/jwtRuntime.js +19 -0
  99. package/dist/nodefony/src/token/secretFile.js +134 -0
  100. package/dist/nodefony/src/token/tokenCriteria.js +35 -0
  101. package/dist/nodefony/src/token/tokenFilters.js +72 -0
  102. package/dist/nodefony/src/token/tokenSort.js +40 -0
  103. package/dist/nodefony/src/token/tokenStatus.js +35 -0
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
  106. package/dist/nodefony/src/totp/totpCipher.js +30 -0
  107. package/dist/nodefony/src/totp/totpCrypto.js +226 -0
  108. package/dist/nodefony/src/totp/totpOperations.js +129 -0
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
  110. package/dist/nodefony/src/voter/RoleVoter.js +32 -0
  111. package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
  112. package/dist/nodefony/src/voter/voterRegistry.js +20 -0
  113. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
  114. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
  115. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
  116. package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
  117. package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
  118. package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
  119. package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
  120. package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
  121. package/dist/nodefony/src/webhook/webhookSort.js +48 -0
  122. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
  123. package/dist/types/index.d.ts +157 -0
  124. package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
  125. package/dist/types/nodefony/command/security-token.d.ts +44 -0
  126. package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
  127. package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
  128. package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
  129. package/dist/types/nodefony/config/config.d.ts +295 -0
  130. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  131. package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
  132. package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
  133. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
  134. package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
  135. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
  136. package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
  137. package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
  138. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
  139. package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
  140. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
  141. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
  142. package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
  143. package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
  144. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
  145. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
  146. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
  147. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
  148. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
  149. package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
  150. package/dist/types/nodefony/contracts/index.d.ts +9 -0
  151. package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
  152. package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
  153. package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
  154. package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
  155. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
  156. package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
  157. package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
  158. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
  159. package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
  160. package/dist/types/nodefony/errors/index.d.ts +8 -0
  161. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
  162. package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
  163. package/dist/types/nodefony/service/auditService.d.ts +30 -0
  164. package/dist/types/nodefony/service/authFlow.d.ts +123 -0
  165. package/dist/types/nodefony/service/authorization.d.ts +33 -0
  166. package/dist/types/nodefony/service/cors.d.ts +48 -0
  167. package/dist/types/nodefony/service/csrf.d.ts +57 -0
  168. package/dist/types/nodefony/service/firewall.d.ts +148 -0
  169. package/dist/types/nodefony/service/oauth2.d.ts +66 -0
  170. package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
  171. package/dist/types/nodefony/service/tokenService.d.ts +103 -0
  172. package/dist/types/nodefony/service/totp.d.ts +58 -0
  173. package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
  174. package/dist/types/nodefony/service/webhooks.d.ts +160 -0
  175. package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
  176. package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
  177. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
  178. package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
  179. package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
  180. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
  181. package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
  182. package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
  183. package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
  184. package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
  185. package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
  186. package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
  187. package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
  188. package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
  189. package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
  190. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
  191. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
  192. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
  193. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
  194. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
  195. package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
  196. package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
  197. package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
  198. package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
  199. package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
  200. package/dist/types/nodefony/src/csp.d.ts +39 -0
  201. package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
  202. package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
  203. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
  204. package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
  205. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
  206. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
  207. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
  208. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
  209. package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
  210. package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
  211. package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
  212. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
  213. package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
  214. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
  215. package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
  216. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
  217. package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
  218. package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
  219. package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
  220. package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
  221. package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
  222. package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
  223. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
  224. package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
  225. package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
  226. package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
  227. package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
  228. package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
  229. package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
  230. package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
  231. package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
  232. package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
  233. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
  234. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
  235. package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
  236. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
  237. package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
  238. package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
  239. package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
  240. package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
  241. package/docs/api-keys.md +691 -0
  242. package/docs/audit.md +751 -0
  243. package/docs/authenticators.md +487 -0
  244. package/docs/authorization.md +497 -0
  245. package/docs/cors.md +497 -0
  246. package/docs/csrf.md +392 -0
  247. package/docs/external-jwt.md +181 -0
  248. package/docs/firewall.md +546 -0
  249. package/docs/headers.md +616 -0
  250. package/docs/index.md +207 -0
  251. package/docs/lexique.md +190 -0
  252. package/docs/oauth2.md +575 -0
  253. package/docs/obtenir-un-jeton.md +225 -0
  254. package/docs/tokens.md +520 -0
  255. package/docs/totp.md +804 -0
  256. package/docs/webauthn.md +733 -0
  257. package/docs/webhooks.md +1016 -0
  258. package/package.json +83 -0
@@ -0,0 +1,495 @@
1
+ import { TOKEN_FACETS, TOKEN_FILTERS, TOKEN_STATS_FILTERS } from "../token/tokenFilters.js";
2
+ import { AUDIT_FILTERS } from "../audit/auditFilters.js";
3
+ import { adminActor, auditAdmin } from "./adminAudit.js";
4
+ import { webhookAdminEndpoints } from "./WebhookAdminApi.js";
5
+ import { parseFilters, parsePageQuery } from "nodefony";
6
+ //#region nodefony/src/admin/SecurityAdminApi.ts
7
+ /**
8
+ * Déduit le **driver** logique d'un store à partir de son nom de classe — le
9
+ * `tokenService` connaît le driver config (`tokenStore.store`) mais ne l'expose
10
+ * pas au container ; on le re-dérive du store réel posé (miroir du pattern
11
+ * `HttpAdminApi.sessions` qui lit `storage.constructor.name`). Mapping fermé sur
12
+ * les implémentations connues ({@link ITokenStore}) ; `null` pour un store tiers
13
+ * inconnu (honnête — on n'invente pas un driver).
14
+ *
15
+ * @param className - `store.constructor.name` (ex. `DrizzleTokenStore`).
16
+ * @returns driver logique, ou `null` si la classe n'est pas reconnue.
17
+ */
18
+ function tokenStoreDriver(className) {
19
+ switch (className) {
20
+ case "MemoryTokenStore": return "memory";
21
+ case "DrizzleTokenStore":
22
+ case "MongooseTokenStore": return "orm";
23
+ case "RedisTokenStore": return "redis";
24
+ default: return null;
25
+ }
26
+ }
27
+ /** Défaut/cap de taille de page du listing admin des clés API. */
28
+ const KEYS_DEFAULT_LIMIT = 50;
29
+ const KEYS_MAX_LIMIT = 200;
30
+ /**
31
+ * Traduit la query string admin en {@link ITokenListQuery} bornée (`limit` par
32
+ * défaut 50, cap 200 ; pagination, tri et filtres de {@link TOKEN_FILTERS}).
33
+ *
34
+ * **UN SEUL traducteur par dimension, jamais deux.** Pour la page et le tri,
35
+ * `parsePageQuery` ; pour les filtres, `parseFilters`. En appeler un second sans
36
+ * son allowlist ferait refuser en 400 ce que le premier venait d'accepter, et
37
+ * aucun test unitaire ne le verrait (chaque appel est correct isolément).
38
+ *
39
+ * Un filtre inconnu ou mal formé est désormais **refusé** (400) au lieu d'être
40
+ * ignoré : `?status=revoqué` rendait la liste ENTIÈRE, que la console affichait
41
+ * comme le résultat du filtre demandé.
42
+ *
43
+ * @param query - `request.query` du broker admin.
44
+ * @param sortable - champs que le backend branché sait trier ; un `?order=`
45
+ * portant autre chose est refusé en 400 par le traducteur. Liste vide (store à
46
+ * curseur, store absent) ⇒ tout tri est refusé, ce qui est la vérité du backend.
47
+ */
48
+ function parseTokenListQuery(query, sortable) {
49
+ const page = parsePageQuery(query, {
50
+ defaultLimit: KEYS_DEFAULT_LIMIT,
51
+ maxLimit: KEYS_MAX_LIMIT,
52
+ sortable
53
+ });
54
+ const filters = parseFilters(query, TOKEN_FILTERS);
55
+ const out = {
56
+ limit: page.limit,
57
+ ...filters
58
+ };
59
+ if (page.offset !== void 0) out.offset = page.offset;
60
+ if (page.cursor !== void 0) out.cursor = page.cursor;
61
+ if (page.order !== void 0) out.order = page.order;
62
+ return out;
63
+ }
64
+ /** Premier param d'une clé (la query admin peut être `string | string[]`). */
65
+ function one(query, key) {
66
+ const value = query[key];
67
+ if (value === void 0) return void 0;
68
+ return Array.isArray(value) ? value[0] : value;
69
+ }
70
+ /** Taille de page du journal d'audit quand l'appelant n'en demande pas. */
71
+ const AUDIT_DEFAULT_LIMIT = 100;
72
+ /**
73
+ * Traduit la query string admin en {@link IAuditListQuery} typée — pagination
74
+ * par curseur, plus les filtres de {@link AUDIT_FILTERS}.
75
+ *
76
+ * Un filtre inconnu ou mal formé est **refusé** (400). Il était auparavant
77
+ * ignoré, au nom de la robustesse de la console : mais un journal d'audit rendu
78
+ * ENTIER à qui demandait `?outcome=deneid` n'est pas robuste — c'est la pire
79
+ * réponse possible à un auditeur, qui lit l'absence de refus comme l'absence
80
+ * d'incident. Le typage suit la même source : les valeurs viennent de la spec,
81
+ * il n'y a plus de `as AuditCategory` à écrire ici.
82
+ *
83
+ * Le `limit` est **toujours posé** (défaut {@link AUDIT_DEFAULT_LIMIT}, cap du
84
+ * traducteur) : le contrat de page n'admet pas « tout » ; le store applique en
85
+ * plus son propre plafond, l'appelant ne peut donc pas s'en servir pour tirer un
86
+ * journal entier.
87
+ *
88
+ * @param query - `request.query` du broker admin.
89
+ * @returns filtre prêt pour `auditService.listPage`.
90
+ * @throws `PageQueryError` (400) sur un filtre inconnu ou mal formé.
91
+ */
92
+ function parseAuditQuery(query) {
93
+ const page = parsePageQuery(query, { defaultLimit: AUDIT_DEFAULT_LIMIT });
94
+ const filter = {
95
+ limit: page.limit,
96
+ ...parseFilters(query, AUDIT_FILTERS)
97
+ };
98
+ if (page.cursor !== void 0) filter.cursor = page.cursor;
99
+ return filter;
100
+ }
101
+ /** Projette un credential serveur vers sa vue admin (sans `publicKey`/`userId`). */
102
+ function toCredentialView(c) {
103
+ return {
104
+ id: c.id,
105
+ nickname: c.nickname ?? null,
106
+ transports: c.transports,
107
+ backupEligible: c.backupEligible,
108
+ backupState: c.backupState,
109
+ uvInitialized: c.uvInitialized,
110
+ createdAt: c.createdAt,
111
+ lastUsedAt: c.lastUsedAt
112
+ };
113
+ }
114
+ /**
115
+ * Producteur admin (`IAdminApi`) du module sécurité — data plane consommé par
116
+ * Studio (section Sécurité, P6.15) :
117
+ *
118
+ * - `GET /nodefony/security/api/audit/events` — page filtrée du journal d'audit
119
+ * (P6.14 Lot 3 ; `?category&outcome&actor&action&requestId&since&until&limit&cursor`),
120
+ * du plus récent au plus ancien, pagination par curseur (`cursor` / `nextCursor`).
121
+ * - `GET /nodefony/security/api/firewall` — introspection du firewall (zones,
122
+ * authenticators montés, défenses) — état RUNTIME, secrets exclus.
123
+ * - `GET /nodefony/security/api/roleHierarchy` — hiérarchie de rôles + résolution.
124
+ * - `GET /nodefony/security/api/apikeys` — toutes les clés API (gouvernance),
125
+ * `GET …/apikeys/status` — backend du token store (« où on écrit »),
126
+ * `POST …/apikeys/{id}/revoke` — révocation par id (réponse à incident).
127
+ * - `GET …/users/{id}/passkeys` + `DELETE …/users/{id}/passkeys/{credentialId}`
128
+ * — passkeys d'un utilisateur (vue admin sans clé publique) + révocation
129
+ * owner-scopée (reset facteur fort, audité).
130
+ * - `GET …/users/{id}/totp` + `POST …/users/{id}/totp/disable` — état du 2FA
131
+ * TOTP + désactivation (reset facteur fort, audité). Pas d'enrôlement
132
+ * cross-user (le secret se scanne sur l'appareil de l'utilisateur).
133
+ *
134
+ * **RBAC `ROLE_NODEFONY_ADMIN`** (appliqué par le broker, 403 sinon) — la console
135
+ * sécurité ne se consulte qu'en administrateur. Handlers **lazy** : ils résolvent
136
+ * `auditService`/`firewall` à la requête (service désactivable → 503), jamais au
137
+ * montage. Le namespace `"security"` est distinct des routes classiques
138
+ * `/nodefony/security/api/keys` (P6.12) — paths disjoints, zéro collision.
139
+ *
140
+ * @param container - container du kernel (résolution lazy des services).
141
+ */
142
+ function createSecurityAdminApi(container) {
143
+ const endpoints = [
144
+ {
145
+ path: "audit/events",
146
+ method: "GET",
147
+ role: "ROLE_NODEFONY_ADMIN",
148
+ summary: "Journal d'audit de sécurité (login/refus/jetons). Filtres : ?category&outcome&actor&action&since&until&limit&cursor (curseur).",
149
+ handler: async (request) => {
150
+ const audit = container.get("auditService");
151
+ if (!audit || !audit.isEnabled()) return {
152
+ status: 503,
153
+ body: { error: "audit journal unavailable" }
154
+ };
155
+ return audit.listPage(parseAuditQuery(request.query));
156
+ }
157
+ },
158
+ {
159
+ path: "firewall",
160
+ method: "GET",
161
+ role: "ROLE_NODEFONY_ADMIN",
162
+ summary: "Introspection du firewall : zones (URL/host/authenticators), authenticators montés, défenses (CSRF/CORS/en-têtes/throttle). Secrets exclus (présence, jamais valeur).",
163
+ handler: () => {
164
+ const firewall = container.get("firewall");
165
+ if (!firewall) return {
166
+ status: 503,
167
+ body: { error: "firewall unavailable" }
168
+ };
169
+ return firewall.describe();
170
+ }
171
+ },
172
+ {
173
+ path: "roleHierarchy",
174
+ method: "GET",
175
+ role: "ROLE_NODEFONY_ADMIN",
176
+ summary: "Hiérarchie de rôles déclarée + résolution transitive (RoleHierarchyWalker, niveau A de l'autorisation).",
177
+ handler: () => {
178
+ const firewall = container.get("firewall");
179
+ if (!firewall) return {
180
+ status: 503,
181
+ body: { error: "firewall unavailable" }
182
+ };
183
+ return firewall.describeRoleHierarchy();
184
+ }
185
+ },
186
+ {
187
+ path: "apikeys",
188
+ method: "GET",
189
+ role: "ROLE_NODEFONY_ADMIN",
190
+ summary: "Clés API (PAT) du système — gouvernance cross-porteur, pagination NATIVE au store (?subjectId&revoked&limit&offset|cursor&order=champ:ASC). Tri limité aux champs que le backend branché déclare savoir trier. Vue publique, sans secret.",
191
+ page: {
192
+ sortable: () => {
193
+ const svc = container.get("apiKeys");
194
+ return svc?.isEnabled() ? svc.sortableFields() : [];
195
+ },
196
+ filters: TOKEN_FILTERS
197
+ },
198
+ handler: async (request) => {
199
+ const svc = container.get("apiKeys");
200
+ if (!svc || !svc.isEnabled()) return {
201
+ status: 503,
202
+ body: { error: "api keys unavailable" }
203
+ };
204
+ const page = await svc.listPagePat(parseTokenListQuery(request.query, svc.sortableFields()));
205
+ return {
206
+ keys: page.items,
207
+ total: page.total,
208
+ limit: page.limit,
209
+ offset: page.offset,
210
+ nextCursor: page.nextCursor
211
+ };
212
+ }
213
+ },
214
+ {
215
+ path: "apikeys/stats",
216
+ method: "GET",
217
+ role: "ROLE_NODEFONY_ADMIN",
218
+ summary: "Compteurs des clés d'API sur la collection ENTIÈRE (total, actives, expirées, révoquées) — mêmes filtres que la liste. `null` = le backend ne sait pas compter (store Redis en curseur).",
219
+ page: {
220
+ filters: TOKEN_STATS_FILTERS,
221
+ facets: TOKEN_FACETS
222
+ },
223
+ handler: async (request) => {
224
+ const svc = container.get("apiKeys");
225
+ if (!svc || !svc.isEnabled()) return {
226
+ status: 503,
227
+ body: { error: "api keys unavailable" }
228
+ };
229
+ parsePageQuery(request.query, {});
230
+ return svc.countKeyFacets(parseFilters(request.query, TOKEN_STATS_FILTERS));
231
+ }
232
+ },
233
+ {
234
+ path: "apikeys/status",
235
+ method: "GET",
236
+ role: "ROLE_NODEFONY_ADMIN",
237
+ summary: "Statut du sous-système clés API : « où on écrit » = backend du token store (classe réelle + driver memory/orm/redis). Sans secret.",
238
+ handler: () => {
239
+ const enabled = container.get("apiKeys")?.isEnabled() ?? false;
240
+ const className = container.get("tokenStore")?.constructor?.name;
241
+ return {
242
+ enabled,
243
+ store: className ?? "none",
244
+ driver: className ? tokenStoreDriver(className) : null
245
+ };
246
+ }
247
+ },
248
+ {
249
+ path: "apikeys/{id}/revoke",
250
+ method: "POST",
251
+ role: "ROLE_NODEFONY_ADMIN",
252
+ summary: "Révoque une clé API par id (réponse à incident : clé compromise) — audité (acteur admin + porteur cible). 404 si introuvable.",
253
+ handler: async (request) => {
254
+ const svc = container.get("apiKeys");
255
+ if (!svc || !svc.isEnabled()) return {
256
+ status: 503,
257
+ body: { error: "api keys unavailable" }
258
+ };
259
+ const id = request.params.id;
260
+ if (typeof id !== "string" || id.length === 0) return {
261
+ status: 404,
262
+ body: { error: "not found" }
263
+ };
264
+ const key = await svc.revokeAnyPat(id, adminActor(request.user));
265
+ if (!key) return {
266
+ status: 404,
267
+ body: { error: "not found" }
268
+ };
269
+ return {
270
+ ok: true,
271
+ key
272
+ };
273
+ }
274
+ },
275
+ {
276
+ path: "webauthn/list",
277
+ method: "GET",
278
+ role: "ROLE_NODEFONY_ADMIN",
279
+ summary: "Passkeys de la plateforme (id, porteur, transports, sauvegarde, compteur anti-clone, dates — jamais la clé publique). Paginé serveur : ?userId&backedUp&q&limit&offset. `enabled:false` = passkeys désactivés en config. `total:-1` = backend sans comptage (Redis).",
280
+ handler: async (request) => {
281
+ const svc = container.get("webauthn");
282
+ const pageQuery = parsePageQuery(request.query, {
283
+ defaultLimit: KEYS_DEFAULT_LIMIT,
284
+ maxLimit: KEYS_MAX_LIMIT,
285
+ searchable: true
286
+ });
287
+ const limit = pageQuery.limit;
288
+ const offset = pageQuery.offset ?? 0;
289
+ if (!svc) return {
290
+ enabled: false,
291
+ items: [],
292
+ total: 0,
293
+ limit,
294
+ offset
295
+ };
296
+ const userId = one(request.query, "userId");
297
+ const backedUp = one(request.query, "backedUp");
298
+ const q = pageQuery.q;
299
+ const page = await svc.listCredentialsPage({
300
+ limit,
301
+ offset,
302
+ ...userId !== void 0 ? { userId } : {},
303
+ ...backedUp === "true" ? { backedUp: true } : backedUp === "false" ? { backedUp: false } : {},
304
+ ...q !== void 0 ? { q } : {}
305
+ });
306
+ return {
307
+ enabled: true,
308
+ items: page.items,
309
+ total: page.total,
310
+ limit,
311
+ offset
312
+ };
313
+ }
314
+ },
315
+ {
316
+ path: "users/{id}/passkeys",
317
+ method: "GET",
318
+ role: "ROLE_NODEFONY_ADMIN",
319
+ summary: "Passkeys (WebAuthn) d'un utilisateur — vue admin SANS clé publique (id/surnom/transports/sauvegarde/UV/dates). 404 si utilisateur inconnu.",
320
+ handler: async (request) => {
321
+ const svc = container.get("webauthn");
322
+ if (!svc) return {
323
+ status: 503,
324
+ body: { error: "webauthn unavailable" }
325
+ };
326
+ const id = request.params.id;
327
+ if (typeof id !== "string" || id.length === 0) return {
328
+ status: 404,
329
+ body: { error: "not found" }
330
+ };
331
+ const users = container.get("users");
332
+ if (users && !await users.findById(id)) return {
333
+ status: 404,
334
+ body: { error: "not found" }
335
+ };
336
+ return { credentials: (await svc.listUserCredentials(id)).map(toCredentialView) };
337
+ }
338
+ },
339
+ {
340
+ path: "users/{id}/passkeys/{credentialId}",
341
+ method: "DELETE",
342
+ role: "ROLE_NODEFONY_ADMIN",
343
+ summary: "Révoque une passkey d'un utilisateur (reset admin) — audité. 404 si la passkey n'existe pas ou n'appartient pas à l'utilisateur (anti-IDOR).",
344
+ handler: async (request) => {
345
+ const svc = container.get("webauthn");
346
+ if (!svc) return {
347
+ status: 503,
348
+ body: { error: "webauthn unavailable" }
349
+ };
350
+ const id = request.params.id;
351
+ const credentialId = request.params.credentialId;
352
+ if (typeof id !== "string" || id.length === 0 || typeof credentialId !== "string" || credentialId.length === 0) return {
353
+ status: 404,
354
+ body: { error: "not found" }
355
+ };
356
+ if (!await svc.removeUserCredential(id, credentialId)) return {
357
+ status: 404,
358
+ body: { error: "not found" }
359
+ };
360
+ auditAdmin(container, {
361
+ category: "webauthn",
362
+ action: "user.passkey_revoked",
363
+ outcome: "success",
364
+ actor: adminActor(request.user),
365
+ resource: id,
366
+ metadata: {
367
+ credentialId,
368
+ viaAdmin: true
369
+ }
370
+ });
371
+ return { ok: true };
372
+ }
373
+ },
374
+ {
375
+ path: "totp/list",
376
+ method: "GET",
377
+ role: "ROLE_NODEFONY_ADMIN",
378
+ summary: "Enrôlements 2FA TOTP (userId, paramètres, confirmation, codes de récupération RESTANTS — jamais le secret). Paginé serveur : ?confirmed&q&limit&offset. `enabled:false` = 2FA désactivé en config.",
379
+ handler: async (request) => {
380
+ const svc = container.get("totp");
381
+ const pageQuery = parsePageQuery(request.query, {
382
+ defaultLimit: KEYS_DEFAULT_LIMIT,
383
+ maxLimit: KEYS_MAX_LIMIT
384
+ });
385
+ const limit = pageQuery.limit;
386
+ const offset = pageQuery.offset ?? 0;
387
+ if (!svc) return {
388
+ enabled: false,
389
+ items: [],
390
+ total: 0,
391
+ limit,
392
+ offset
393
+ };
394
+ const confirmed = one(request.query, "confirmed");
395
+ const q = one(request.query, "q");
396
+ const page = await svc.listPage({
397
+ limit,
398
+ offset,
399
+ ...confirmed === "true" ? { confirmed: true } : confirmed === "false" ? { confirmed: false } : {},
400
+ ...q !== void 0 ? { q } : {}
401
+ });
402
+ return {
403
+ enabled: true,
404
+ items: page.items,
405
+ total: page.total,
406
+ limit,
407
+ offset
408
+ };
409
+ }
410
+ },
411
+ {
412
+ path: "users/{id}/totp",
413
+ method: "GET",
414
+ role: "ROLE_NODEFONY_ADMIN",
415
+ summary: "État du 2FA TOTP d'un utilisateur (activé / en attente / codes de récupération restants). 404 si utilisateur inconnu.",
416
+ handler: async (request) => {
417
+ const svc = container.get("totp");
418
+ if (!svc) return {
419
+ status: 503,
420
+ body: { error: "totp unavailable" }
421
+ };
422
+ const id = request.params.id;
423
+ if (typeof id !== "string" || id.length === 0) return {
424
+ status: 404,
425
+ body: { error: "not found" }
426
+ };
427
+ const users = container.get("users");
428
+ if (users && !await users.findById(id)) return {
429
+ status: 404,
430
+ body: { error: "not found" }
431
+ };
432
+ return svc.status(id);
433
+ }
434
+ },
435
+ {
436
+ path: "users/{id}/totp/disable",
437
+ method: "POST",
438
+ role: "ROLE_NODEFONY_ADMIN",
439
+ summary: "Désactive le 2FA TOTP d'un utilisateur (reset admin : appareil perdu) — audité. Idempotent (no-op si déjà désactivé).",
440
+ handler: async (request) => {
441
+ const svc = container.get("totp");
442
+ if (!svc) return {
443
+ status: 503,
444
+ body: { error: "totp unavailable" }
445
+ };
446
+ const id = request.params.id;
447
+ if (typeof id !== "string" || id.length === 0) return {
448
+ status: 404,
449
+ body: { error: "not found" }
450
+ };
451
+ const users = container.get("users");
452
+ if (users && !await users.findById(id)) return {
453
+ status: 404,
454
+ body: { error: "not found" }
455
+ };
456
+ await svc.disable(id);
457
+ auditAdmin(container, {
458
+ category: "auth",
459
+ action: "user.totp_disabled",
460
+ outcome: "success",
461
+ actor: adminActor(request.user),
462
+ resource: id,
463
+ metadata: { viaAdmin: true }
464
+ });
465
+ return { ok: true };
466
+ }
467
+ },
468
+ ...webhookAdminEndpoints(container)
469
+ ];
470
+ const descriptor = {
471
+ label: "Sécurité",
472
+ icon: "shield-lock",
473
+ order: 15,
474
+ role: "ROLE_NODEFONY_ADMIN"
475
+ };
476
+ return {
477
+ adminNamespace: "security",
478
+ adminDescriptor: () => descriptor,
479
+ adminEndpoints: () => endpoints
480
+ };
481
+ }
482
+ /**
483
+ * Enregistre le producteur admin sécurité sur le broker — **idempotent** (no-op
484
+ * si déjà monté). À appeler au `onKernelBoot` du module (avant le montage des
485
+ * routes par framework à `onKernelReady`). Calque `registerOrmAdminApi`.
486
+ *
487
+ * @param registry - broker admin (`container.get("adminBroker")`).
488
+ * @param container - container du kernel (capturé par le handler lazy).
489
+ */
490
+ function registerSecurityAdminApi(registry, container) {
491
+ if (registry.has("security")) return;
492
+ registry.register(createSecurityAdminApi(container));
493
+ }
494
+ //#endregion
495
+ export { createSecurityAdminApi, parseAuditQuery, parseTokenListQuery, registerSecurityAdminApi };