@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4

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 (249) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +48 -16
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
  4. package/dist/nodefony/command/security-oauth-doctor.js +68 -0
  5. package/dist/nodefony/command/security-secrets.js +52 -29
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +10 -7
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +9 -14
  10. package/dist/nodefony/config/config.js +99 -8
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/contracts/index.js +1 -2
  14. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  15. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  16. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  17. package/dist/nodefony/errors/CsrfError.js +1 -2
  18. package/dist/nodefony/errors/InsecureTransportError.js +23 -0
  19. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  20. package/dist/nodefony/errors/SsrfError.js +1 -2
  21. package/dist/nodefony/errors/ThrottledError.js +1 -2
  22. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  23. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  24. package/dist/nodefony/errors/index.js +2 -1
  25. package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
  26. package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
  27. package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
  28. package/dist/nodefony/keycloak/scaffold.js +231 -0
  29. package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
  30. package/dist/nodefony/scaffold/contribute.js +19 -0
  31. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  32. package/dist/nodefony/service/apiKeys.js +45 -5
  33. package/dist/nodefony/service/auditService.js +4 -5
  34. package/dist/nodefony/service/authFlow.js +118 -16
  35. package/dist/nodefony/service/authorization.js +1 -2
  36. package/dist/nodefony/service/cors.js +1 -2
  37. package/dist/nodefony/service/csrf.js +1 -2
  38. package/dist/nodefony/service/firewall.js +116 -25
  39. package/dist/nodefony/service/keycloak.js +208 -0
  40. package/dist/nodefony/service/oauth2.js +403 -28
  41. package/dist/nodefony/service/securityHeaders.js +1 -2
  42. package/dist/nodefony/service/tokenService.js +94 -27
  43. package/dist/nodefony/service/totp.js +55 -11
  44. package/dist/nodefony/service/webAuthn.js +24 -7
  45. package/dist/nodefony/service/webhooks.js +4 -5
  46. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  47. package/dist/nodefony/src/SecuredArea.js +15 -5
  48. package/dist/nodefony/src/accountBinding.js +96 -0
  49. package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
  50. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  51. package/dist/nodefony/src/admin/adminAudit.js +1 -3
  52. package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
  53. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  54. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  55. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  56. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  57. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  58. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  59. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  60. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  61. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  62. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
  63. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  64. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
  65. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
  66. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  67. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
  68. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
  69. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  70. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  71. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  72. package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
  73. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  74. package/dist/nodefony/src/csp.js +1 -2
  75. package/dist/nodefony/src/csrfToken.js +1 -2
  76. package/dist/nodefony/src/entryPoint.js +38 -0
  77. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  78. package/dist/nodefony/src/net/transportFailure.js +57 -0
  79. package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
  80. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  81. package/dist/nodefony/src/oauth/metadata.js +28 -12
  82. package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
  83. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
  84. package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
  85. package/dist/nodefony/src/oauth/providerRoles.js +78 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +48 -12
  87. package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
  88. package/dist/nodefony/src/openToSignup.js +46 -0
  89. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  90. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  91. package/dist/nodefony/src/rolesWithin.js +57 -0
  92. package/dist/nodefony/src/sessionAuthentication.js +25 -0
  93. package/dist/nodefony/src/sessionIdentity.js +29 -3
  94. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  95. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  96. package/dist/nodefony/src/token/JwtKeystore.js +89 -48
  97. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  98. package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
  99. package/dist/nodefony/src/token/UserToken.js +1 -2
  100. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  101. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  102. package/dist/nodefony/src/token/secretFile.js +60 -4
  103. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  104. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  105. package/dist/nodefony/src/token/tokenSort.js +1 -2
  106. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  107. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  108. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  109. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  110. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  111. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  112. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  113. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
  114. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  115. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  116. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  117. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  118. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  119. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  120. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  124. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  125. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  126. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  127. package/dist/types/index.d.ts +26 -6
  128. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
  129. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  130. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  131. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  132. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  133. package/dist/types/nodefony/config/config.d.ts +64 -1
  134. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  135. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  136. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  137. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  138. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  139. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  140. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  141. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  142. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  143. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  144. package/dist/types/nodefony/errors/index.d.ts +1 -0
  145. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  146. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  147. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  148. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  149. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  150. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  151. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  152. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  153. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  154. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  155. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  156. package/dist/types/nodefony/service/oauth2.d.ts +145 -9
  157. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  158. package/dist/types/nodefony/service/totp.d.ts +28 -4
  159. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  160. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  161. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  162. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
  163. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
  164. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  165. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
  166. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  167. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  168. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  169. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  170. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  171. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  172. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  173. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  174. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  175. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  176. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  177. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  178. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  179. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  180. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  181. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  182. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  183. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  184. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  185. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  186. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  187. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  188. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  189. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  190. package/docs/api-keys.md +182 -175
  191. package/docs/audit.md +141 -141
  192. package/docs/authenticators.md +129 -96
  193. package/docs/authorization.md +116 -69
  194. package/docs/cors.md +64 -64
  195. package/docs/csrf.md +77 -72
  196. package/docs/external-jwt.md +83 -11
  197. package/docs/firewall.md +119 -83
  198. package/docs/headers.md +82 -82
  199. package/docs/index.md +16 -8
  200. package/docs/keycloak.md +835 -0
  201. package/docs/login-page.md +361 -0
  202. package/docs/oauth2.md +291 -166
  203. package/docs/obtenir-un-jeton.md +131 -34
  204. package/docs/tokens.md +197 -140
  205. package/docs/totp.md +177 -136
  206. package/docs/webauthn.md +157 -154
  207. package/docs/webhooks.md +128 -125
  208. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  209. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  210. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  211. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  212. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  213. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  214. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  215. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  216. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  217. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  218. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  219. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  220. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  221. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  222. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  223. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  224. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  225. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  226. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  227. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  228. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  229. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  230. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  231. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  232. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  233. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  234. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  235. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  236. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  237. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  238. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  239. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  240. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  241. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  242. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  243. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  244. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  245. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  246. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  247. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  248. package/package.json +19 -17
  249. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/audit.md CHANGED
@@ -30,8 +30,8 @@ source: "src/packages/@nodefony/security/docs/audit.md"
30
30
  > Chaque fois qu'une **décision de sécurité** est prise — un login réussit, un accès est refusé, une
31
31
  > clé d'API est révoquée, un jeton volé resurgit — Nodefony écrit une ligne dans un journal
32
32
  > **append-only** : qui, quoi, quand, d'où, avec quel verdict. Pas le trafic (ça, c'est le log HTTP) :
33
- > les **transitions d'état**. Ancré sur `AuditService` (`auditService.ts:51`), le contrat
34
- > `IAuditEvent` (`IAuditEvent.ts:61`) et les stores de `nodefony/src/audit/`.
33
+ > les **transitions d'état**. Ancré sur `AuditService` (`auditService.ts#AuditService.#store`), le contrat
34
+ > `IAuditEvent` (`IAuditEvent.ts#IAuditEvent`) et les stores de `nodefony/src/audit/`.
35
35
 
36
36
  📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Journal d'audit**
37
37
 
@@ -70,7 +70,7 @@ flowchart LR
70
70
  API --> UI["Studio — Journal d'audit"]
71
71
  ```
72
72
 
73
- `AuditService.record()` (`auditService.ts:187`) est le point de passage unique : il pose l'identité et
73
+ `AuditService.record()` (`auditService.ts#AuditService.record`) est le point de passage unique : il pose l'identité et
74
74
  l'horodatage, écrit **sans attendre**, et notifie les abonnés live. Tout le reste — stores, pont WS,
75
75
  webhooks, console — se branche autour de lui.
76
76
 
@@ -118,24 +118,24 @@ Trois partis pris, tous vérifiables dans le code.
118
118
 
119
119
  **1. Le journal trace les transitions, jamais le trafic.** Le succès nominal est **muet** : une
120
120
  requête authentifiée qui passe n'écrit rien. Prouvé par les tests « SUCCÈS authentifié → AUCUNE
121
- émission » (`auditEmissionHotPath.test.ts:229`) et « SUCCÈS anonyme EXPLICITE → AUCUNE émission »
122
- (`auditEmissionHotPath.test.ts:243`). Conséquence directe : ce que tu lis dans le journal **est** un
121
+ émission » (`auditEmissionHotPath.test.ts`) et « SUCCÈS anonyme EXPLICITE → AUCUNE émission »
122
+ (`auditEmissionHotPath.test.ts`). Conséquence directe : ce que tu lis dans le journal **est** un
123
123
  signal, pas du bruit à filtrer.
124
124
 
125
125
  **2. L'audit ne peut jamais casser le métier.** `record()` est synchrone, sans `await`, et l'écriture
126
- part en fire-and-forget avec un `.catch()` qui se contente de logger (`auditService.ts:199`). Un
126
+ part en fire-and-forget avec un `.catch()` qui se contente de logger (`auditService.ts#AuditService.record`). Un
127
127
  store en panne dégrade la traçabilité, il ne renvoie pas 500 à l'utilisateur.
128
128
 
129
129
  **3. Le store est pluggable, jamais câblé en dur.** Le service résout un **nom** via un registre
130
- (`getAuditStoreFactory()`, `auditStoreRegistry.ts:48`) ; le socle n'embarque que le builtin mémoire,
131
- posé par `registerAuditStore("memory")` à l'import (`auditStoreRegistry.ts:64`), et les backends
130
+ (`getAuditStoreFactory()`, `auditStoreRegistry.ts#getAuditStoreFactory`) ; le socle n'embarque que le builtin mémoire,
131
+ posé par `registerAuditStore("memory")` à l'import (`auditStoreRegistry.ts#registerAuditStore`), et les backends
132
132
  lourds s'enregistrent depuis **leur** module. Un
133
133
  `if (name === "drizzle")` dans le service trahirait la promesse.
134
134
 
135
135
  ## 🚀 Démarrage rapide
136
136
 
137
137
  Dans une app générée par `nodefony create app`, l'audit est **déjà actif** (`enabled: true` par
138
- défaut, `config.ts:729`) sur le store que résout `auto` : la base SQLite locale de l'application
138
+ défaut, `config.ts#resourceServerSchema`) sur le store que résout `auto` : la base SQLite locale de l'application
139
139
  générée, la mémoire seulement si aucun ORM n'est chargé. Voici le parcours complet : configurer, émettre,
140
140
  relire.
141
141
 
@@ -155,11 +155,11 @@ use("@nodefony/security", {
155
155
  ```
156
156
 
157
157
  Rien d'autre à écrire : l'adapter `@nodefony/drizzle` déclare l'entité **et** la fabrique de store
158
- tout seul au démarrage (`registerStores.ts:241`).
158
+ tout seul au démarrage (`registerStores.ts#registerMongooseFrameworkStores`).
159
159
 
160
160
  ### 2. Émettre un événement depuis ton code
161
161
 
162
- `recordAudit()` (`recordAudit.ts:15`) est exporté par le module. Il est **no-op** si l'audit est
162
+ `recordAudit()` (`recordAudit.ts#recordAudit`) est exporté par le module. Il est **no-op** si l'audit est
163
163
  absent ou désactivé — tu peux l'appeler sans garde.
164
164
 
165
165
  ```typescript
@@ -197,7 +197,7 @@ export default ExportController;
197
197
  ```
198
198
 
199
199
  > [!IMPORTANT]
200
- > `category` est une **union fermée** de sous-systèmes de sécurité (`IAuditEvent.ts:71`) : il n'existe
200
+ > `category` est une **union fermée** de sous-systèmes de sécurité (`IAuditEvent.ts#IAuditEvent.category`) : il n'existe
201
201
  > pas de catégorie « métier ». C'est volontaire — ce journal est celui de la sécurité. Range ton
202
202
  > événement dans la catégorie de sécurité qu'il concerne (ici `authz` : un accès privilégié à des
203
203
  > données) et laisse `action` porter le vocabulaire métier.
@@ -205,7 +205,7 @@ export default ExportController;
205
205
  ### 3. Relire le journal
206
206
 
207
207
  Le data plane d'admin sert une **page filtrée**, réservée à `ROLE_NODEFONY_ADMIN`
208
- (`SecurityAdminApi.ts:331`) :
208
+ (`SecurityAdminApi.ts#createSecurityAdminApi`) :
209
209
 
210
210
  ```bash
211
211
  # Les échecs d'authentification d'un compte, sur une fenêtre donnée
@@ -255,7 +255,7 @@ et ratées, sur une fenêtre précise.
255
255
 
256
256
  Aucune configuration : les événements d'authentification sont émis par défaut. Il suffit de filtrer.
257
257
  Les critères se **combinent en ET** et sont traduits par `parseAuditQuery()`
258
- (`SecurityAdminApi.ts:184`) :
258
+ (`SecurityAdminApi.ts#parseAuditQuery`) :
259
259
 
260
260
  | Paramètre | Effet | Exemple |
261
261
  | ----------- | ----------------------------------------------- | --------------------------- |
@@ -283,7 +283,7 @@ si le backoff NIST s'est déclenché — la signature d'une attaque par essais r
283
283
  Un auditeur demande la preuve que le contrôle d'accès n'est pas décoratif.
284
284
 
285
285
  Le jury d'autorisation journalise **tout refus** et **seulement** les refus, par un appel à
286
- `recordAudit()` (`authorization.ts:130`) : les octrois restent muets, parce qu'un octroi est du
286
+ `recordAudit()` (`authorization.ts#Authorization.#auditDeny`) : les octrois restent muets, parce qu'un octroi est du
287
287
  volume, pas un signal.
288
288
 
289
289
  ```bash
@@ -313,17 +313,17 @@ use("@nodefony/security", {
313
313
  ```
314
314
 
315
315
  Ce que ça déclenche : le service arme un `GcScheduler` de purge **toutes les heures**
316
- (`auditService.ts:159`, intervalle `GC_INTERVAL_MS` — `auditService.ts:34`), avec gigue
316
+ (`auditService.ts#AuditService.#build`, intervalle `GC_INTERVAL_MS` — `auditService.ts#GC_INTERVAL_MS`), avec gigue
317
317
  anti-avalanche en cluster. Chaque
318
318
  tour appelle la purge du contrat — `gc()`, voisin de `listPage()` dans le même contrat
319
- (`IAuditStore.ts:59`) — qui supprime les événements plus vieux que la fenêtre : un `DELETE` par seuil
320
- côté SQL (`DrizzleAuditStore.ts:256`), un défilement de file tant que l'événement de tête dépasse le
321
- `threshold` côté mémoire (`MemoryAuditStore.ts:133`).
319
+ (`IAuditStore.ts#IAuditStore.listPage`) — qui supprime les événements plus vieux que la fenêtre : un `DELETE` par seuil
320
+ côté SQL (`DrizzleAuditStore.ts#DrizzleAuditStore.gc`), un défilement de file tant que l'événement de tête dépasse le
321
+ `threshold` côté mémoire (`MemoryAuditStore.ts#MemoryAuditStore.gc`).
322
322
 
323
323
  Ce qu'on observe dans les logs : `audit gc — 1284 événement(s) purgé(s)` en niveau DEBUG.
324
324
 
325
325
  Le store mémoire porte une **seconde** protection, indépendante du temps : un plafond de
326
- **10 000 entrées** en FIFO (`MemoryAuditStore.ts:10`, appliqué à `MemoryAuditStore.ts:59`). Ce n'est
326
+ **10 000 entrées** en FIFO (`MemoryAuditStore.ts#DEFAULT_MAX_ENTRIES`, appliqué à `MemoryAuditStore.ts#MemoryAuditStore.append`). Ce n'est
327
327
  pas une politique de rétention, c'est un garde-fou anti-fuite : la mémoire d'un pod est bornée quoi
328
328
  qu'il arrive.
329
329
 
@@ -332,33 +332,33 @@ qu'il arrive.
332
332
  La base est indisponible pendant trente secondes. Que se passe-t-il ?
333
333
 
334
334
  Le choix de Nodefony est explicite : **le métier passe avant la trace**. L'écriture part en
335
- fire-and-forget — `append()` sans `await`, échec absorbé en log ERROR (`auditService.ts:199`) ; côté SQL, si l'ORM n'est
336
- pas connecté, `append()` est un no-op assumé (`DrizzleAuditStore.ts:152`). Un login n'échoue jamais
335
+ fire-and-forget — `append()` sans `await`, échec absorbé en log ERROR (`auditService.ts#AuditService.record`) ; côté SQL, si l'ORM n'est
336
+ pas connecté, `append()` est un no-op assumé (`DrizzleAuditStore.ts#DrizzleAuditStore.append`). Un login n'échoue jamais
337
337
  parce que le journal est cassé.
338
338
 
339
339
  > [!WARNING]
340
340
  > Le corollaire est une **perte possible d'événements** pendant une panne du store. Le contrat est
341
341
  > best-effort, pas transactionnel : il n'existe pas de tampon de reprise ni de journal d'écriture
342
342
  > anticipée. Si ta conformité exige « aucune action sans trace », il faut un store à haute
343
- > disponibilité — le contrat `IAuditStore` (`IAuditStore.ts:48`) est le point d'extension pour ça.
343
+ > disponibilité — le contrat `IAuditStore` (`IAuditStore.ts#IAuditStore`) est le point d'extension pour ça.
344
344
 
345
345
  Deux garde-fous limitent la casse au démarrage : un store **explicitement** configuré mais inconnu
346
- **avorte le boot en production** (`auditService.ts:115`), et un store `memory` en production déclenche
346
+ **avorte le boot en production** (`auditService.ts#AuditService.#build`), et un store `memory` en production déclenche
347
347
  un `WARNING` nommant précisément le risque — volatil, per-pod, rétention réglementaire impossible
348
- (`auditService.ts:128`).
348
+ (`auditService.ts#AuditService.#build`).
349
349
 
350
350
  ## 🔐 Le catalogue des événements audités
351
351
 
352
- Deux axes de classement. La **catégorie** est une union **fermée** (`IAuditEvent.ts:24`) : c'est
352
+ Deux axes de classement. La **catégorie** est une union **fermée** (`IAuditEvent.ts#AuditCategory`) : c'est
353
353
  l'axe de filtrage principal de la console. L'**action** est une chaîne **ouverte**
354
- (`IAuditEvent.ts:74`) : la liste grandit sans jamais casser le contrat.
354
+ (`IAuditEvent.ts#IAuditEvent.action`) : la liste grandit sans jamais casser le contrat.
355
355
 
356
356
  ### Vue d'ensemble — choisir son filtre en cinq secondes
357
357
 
358
358
  <!-- prettier-ignore -->
359
359
  | Catégorie | Ce qu'elle trace | Actions réellement émises par le framework |
360
360
  | --- | --- | --- |
361
- | `auth` | authentification, chaîne du firewall | `auth.failure` · `auth.unverifiable` · `auth.throttled` · `auth.denied` · `login.success` · `login.failure` · `login.throttled` · `login.mfa_required` · `user.totp_disabled` · `user.password_change_self` (par `@nodefony/user`) |
361
+ | `auth` | authentification, chaîne du firewall | `auth.failure` · `auth.unverifiable` · `auth.throttled` · `auth.denied` · `login.success` · `login.failure` · `login.throttled` · `login.mfa_required` · `user.totp_enabled` · `user.totp_disabled` · `user.password_change_self` (par `@nodefony/user`) |
362
362
  | `authz` | autorisation (voters, `@IsGranted`), administration des comptes | `access.denied` · `user.created` · `user.updated` · `user.password_changed` · `user.deleted` (ces quatre-là par `@nodefony/user`) |
363
363
  | `token` | jetons longue durée et clés d'API | `token.issued` · `token.reuse_detected` · `apikey.created` · `apikey.revoked` |
364
364
  | `session` | cycle de vie de session | `logout` · `session.revoked` (révocation depuis l'administration, par `@nodefony/http`) |
@@ -375,7 +375,7 @@ l'importer (`@nodefony/user`, `@nodefony/framework`) : `tests/unit/auditCategory
375
375
  relit les sources de tous les paquets et refuse un littéral hors contrat. Un filtre sur les
376
376
  catégories déclarées voit donc tout le journal.
377
377
 
378
- Les trois issues possibles (`IAuditEvent.ts:43`) ne sont pas interchangeables : `failure` = **l'acteur
378
+ Les trois issues possibles (`IAuditEvent.ts#AuditOutcome`) ne sont pas interchangeables : `failure` = **l'acteur
379
379
  a échoué une preuve** (mauvais mot de passe, signature invalide) ; `denied` = **une politique a
380
380
  refusé** un acteur pourtant bien formé (Zero Trust, rôle manquant) ; `success` = l'action a abouti.
381
381
  Pour un auditeur, la colonne `denied` est celle des tentatives d'accès non autorisé.
@@ -383,80 +383,80 @@ Pour un auditeur, la colonne `denied` est celle des tentatives d'accès non auto
383
383
  ### `auth` — la chaîne d'authentification
384
384
 
385
385
  Quatre sorties d'échec du firewall passent par le même helper `Firewall.#recordAuth()`
386
- (`firewall.ts:907`), qui enrichit l'événement de la provenance et pose la **zone** en `resource` :
386
+ (`firewall.ts#Firewall.#recordAuth`), qui enrichit l'événement de la provenance et pose la **zone** en `resource` :
387
387
 
388
- - `auth.throttled` — backoff NIST déclenché, réponse 429 (`firewall.ts:781`) ;
389
- - `auth.failure` — un credential a été **présenté** et rejeté (`firewall.ts:807`) ;
388
+ - `auth.throttled` — backoff NIST déclenché, réponse 429 (`firewall.ts#Firewall.isSecure`) ;
389
+ - `auth.failure` — un credential a été **présenté** et rejeté (`firewall.ts#Firewall.isSecure`) ;
390
390
  - `auth.unverifiable` — le vérificateur de jetons est indisponible : réponse 503, sans défi
391
- (`firewall.ts:795`) ;
391
+ (`firewall.ts#Firewall.isSecure`) ;
392
392
  - `auth.denied` / `no_credentials` — Zero Trust : rien n'a été présenté sur une zone protégée
393
- (`firewall.ts:822`) ;
394
- - `auth.denied` / `unauthenticated` — un jeton non promu hors `anonymous` (`firewall.ts:850`).
393
+ (`firewall.ts#Firewall.handleSecurity`) ;
394
+ - `auth.denied` / `unauthenticated` — un jeton non promu hors `anonymous` (`firewall.ts#Firewall.handleSecurity`).
395
395
 
396
396
  Le parcours de login BFF émet en parallèle son propre vocabulaire depuis `AuthFlow` :
397
- `login.failure` sur identité inconnue (`authFlow.ts:125`) ou mot de passe faux (`authFlow.ts:153`),
398
- `login.throttled` (`authFlow.ts:139`), `login.mfa_required` quand un second facteur est réclamé
399
- (`authFlow.ts:175`), et `login.success` après la preuve complète (`authFlow.ts:191`,
400
- `authFlow.ts:232`).
397
+ `login.failure` sur identité inconnue (`authFlow.ts#AuthFlow.#loginPage`) ou mot de passe faux (`authFlow.ts#AuthFlow.login`),
398
+ `login.throttled` (`authFlow.ts#AuthFlow.login`), `login.mfa_required` quand un second facteur est réclamé
399
+ (`authFlow.ts#AuthFlow.login`), et `login.success` après la preuve complète (`authFlow.ts#AuthFlow.login`,
400
+ `authFlow.ts#AuthFlow.login`).
401
401
 
402
402
  ### `authz` — le refus d'autorisation
403
403
 
404
404
  Un seul événement, mais c'est le plus parlant : `access.denied`, émis par le jury de voters
405
- (`authorization.ts:130`). `resource` porte l'attribut refusé, `reason` le motif de la décision.
405
+ (`authorization.ts#Authorization.#auditDeny`). `resource` porte l'attribut refusé, `reason` le motif de la décision.
406
406
 
407
407
  ### `token` — la vie et la mort des jetons longue durée
408
408
 
409
409
  - `token.issued` — un couple access/refresh vient d'être émis, donc une surface d'attaque vient
410
- d'être créée ; les `scopes` et l'identifiant du jeton partent en `metadata` (`tokenService.ts:535`) ;
410
+ d'être créée ; les `scopes` et l'identifiant du jeton partent en `metadata` (`tokenService.ts#TokenService.#grantableScopes`) ;
411
411
  - `token.reuse_detected` — **le signal d'attaque le plus fort du journal** : un refresh déjà révoqué
412
412
  a été re-présenté, donc quelqu'un détient un jeton volé. Toute la famille est coupée
413
- (`tokenService.ts:585`, RFC 9700 §4.14) ;
414
- - `apikey.created` / `apikey.revoked` — cycle de vie des clés d'API (`apiKeys.ts:153`,
415
- `apiKeys.ts:212`).
413
+ (`tokenService.ts#TokenService.issueTokens`, RFC 9700 §4.14) ;
414
+ - `apikey.created` / `apikey.revoked` — cycle de vie des clés d'API (`apiKeys.ts#ApiKeyService.createForSubject`,
415
+ `apiKeys.ts#ApiKeyService.listPagePat`).
416
416
 
417
417
  ### `ws` — le verrou de frame WebSocket
418
418
 
419
- `frame.denied` est émis par le rapporteur branché sur le verrou de frame (`firewall.ts:302`) quand une
419
+ `frame.denied` est émis par le rapporteur branché sur le verrou de frame (`firewall.ts#Firewall.#wireRealtime`) quand une
420
420
  socket tente un `api.request` vers une zone protégée ou s'abonne à un canal interdit. La frame ne
421
421
  porte ni IP ni `requestId` — acteur et cible suffisent.
422
422
 
423
423
  ### `webhook` — les mutations d'endpoints et la mort d'un endpoint
424
424
 
425
- Les mutations d'admin sont tracées une par une (`WebhookAdminApi.ts:305` à `WebhookAdminApi.ts:509`),
425
+ Les mutations d'admin sont tracées une par une (`WebhookAdminApi.ts#webhookAdminEndpoints` à `WebhookAdminApi.ts#webhookAdminEndpoints`),
426
426
  y compris `webhook.revealed` : révéler un secret en clair est une action à auditer. L'auto-désactivation
427
427
  après échecs répétés émet **un seul** événement `webhook.disabled` par endpoint qui meurt, jamais un par
428
- échec (`webhooks.ts:587`).
428
+ échec (`webhooks.ts#WebhookService.delete`).
429
429
 
430
430
  ### `config` et Studio — les actions d'admin
431
431
 
432
- Les producteurs d'admin passent par `auditAdmin()` (`adminAudit.ts:37`), qui lit le sink
433
- défensivement, et par `adminActor()` (`adminAudit.ts:19`) pour dériver un libellé d'identité stable
432
+ Les producteurs d'admin passent par `auditAdmin()` (`adminAudit.ts#auditAdmin`), qui lit le sink
433
+ défensivement, et par `adminActor()` (`adminAudit.ts#adminActor`) pour dériver un libellé d'identité stable
434
434
  depuis l'utilisateur de la requête — avec repli `"admin"`, jamais une décision d'autorisation.
435
435
 
436
436
  ## 🧰 Le contrat d'une entrée
437
437
 
438
- Un événement est un objet **sérialisable JSON** (`IAuditEvent.ts:61`). L'émetteur ne fournit qu'un
439
- brouillon `IAuditEventDraft` (`IAuditEvent.ts:113`) : `id` et `ts` sont posés par le service, ce qui
438
+ Un événement est un objet **sérialisable JSON** (`IAuditEvent.ts#IAuditEvent`). L'émetteur ne fournit qu'un
439
+ brouillon `IAuditEventDraft` (`IAuditEvent.ts#IAuditEventDraft`) : `id` et `ts` sont posés par le service, ce qui
440
440
  garantit un seul appel d'horloge, centralisé hors des points d'émission.
441
441
 
442
- | Champ | Type | Posé par | Rôle |
443
- | ----------- | -------------------------- | -------- | ------------------------------------------------------------ |
444
- | `id` | `string` | service | unique dans le process ; sert aussi de composante de curseur |
445
- | `ts` | `number` (epoch ms) | service | horodatage (`auditService.ts:195`) |
446
- | `category` | `AuditCategory` | émetteur | sous-système — union fermée |
447
- | `action` | `string` | émetteur | le fait, `<sujet>.<verbe>` — chaîne ouverte |
448
- | `outcome` | `success\|failure\|denied` | émetteur | le verdict |
449
- | `actor` | `string \| null` | émetteur | libellé d'identité, `null` si anonyme (`IAuditEvent.ts:82`) |
450
- | `resource` | `string \| null` | émetteur | zone, route, canal, attribut — descripteur **léger** |
451
- | `reason` | `string \| null` | émetteur | motif **machine** filtrable, pas un message traduit |
452
- | `ip` | `string \| null` | contexte | provenance réseau |
453
- | `userAgent` | `string \| null` | contexte | provenance client |
454
- | `requestId` | `string \| null` | contexte | corrélation log ↔ audit ↔ trace |
455
- | `flags` | `IAuditEventFlags` | contexte | **présence** de matériel sensible, jamais la valeur |
456
- | `metadata` | `Record<string, unknown>` | émetteur | extras applicatifs libres, absents par défaut |
442
+ | Champ | Type | Posé par | Rôle |
443
+ | ----------- | -------------------------- | -------- | -------------------------------------------------------------------------- |
444
+ | `id` | `string` | service | unique dans le process ; sert aussi de composante de curseur |
445
+ | `ts` | `number` (epoch ms) | service | horodatage (`auditService.ts#AuditService.record`) |
446
+ | `category` | `AuditCategory` | émetteur | sous-système — union fermée |
447
+ | `action` | `string` | émetteur | le fait, `<sujet>.<verbe>` — chaîne ouverte |
448
+ | `outcome` | `success\|failure\|denied` | émetteur | le verdict |
449
+ | `actor` | `string \| null` | émetteur | libellé d'identité, `null` si anonyme (`IAuditEvent.ts#IAuditEvent.actor`) |
450
+ | `resource` | `string \| null` | émetteur | zone, route, canal, attribut — descripteur **léger** |
451
+ | `reason` | `string \| null` | émetteur | motif **machine** filtrable, pas un message traduit |
452
+ | `ip` | `string \| null` | contexte | provenance réseau |
453
+ | `userAgent` | `string \| null` | contexte | provenance client |
454
+ | `requestId` | `string \| null` | contexte | corrélation log ↔ audit ↔ trace |
455
+ | `flags` | `IAuditEventFlags` | contexte | **présence** de matériel sensible, jamais la valeur |
456
+ | `metadata` | `Record<string, unknown>` | émetteur | extras applicatifs libres, absents par défaut |
457
457
 
458
458
  Les quatre derniers champs de provenance sont remplis d'un coup par `readAuditContext()`
459
- (`readAuditContext.ts:33`), qui lit un contexte HTTP ou WS de façon défensive — tous les champs sont
459
+ (`readAuditContext.ts#readAuditContext`), qui lit un contexte HTTP ou WS de façon défensive — tous les champs sont
460
460
  optionnels, un contexte partiel ne casse rien.
461
461
 
462
462
  ## ⚡ Performance & mémoire — pourquoi l'audit ne pèse pas sur la requête
@@ -466,33 +466,33 @@ Quatre mécanismes, tous prouvés par les tests.
466
466
 
467
467
  **1. Le chemin nominal n'émet rien.** Ce n'est pas une optimisation, c'est le modèle : le firewall
468
468
  n'appelle `#recordAuth()` que depuis ses quatre sorties d'échec, jamais depuis le succès
469
- (`firewall.ts:907`). Le verrou WS ne tire sa closure `onDeny` que sur refus (`firewall.ts:358`).
470
- Prouvé : « frame AUTORISÉE → onDeny JAMAIS appelé » (`auditEmissionHotPath.test.ts:333`).
469
+ (`firewall.ts#Firewall.#recordAuth`). Le verrou WS ne tire sa closure `onDeny` que sur refus (`firewall.ts#Firewall.#wireRealtime`).
470
+ Prouvé : « frame AUTORISÉE → onDeny JAMAIS appelé » (`auditEmissionHotPath.test.ts`).
471
471
 
472
472
  **2. Audit désactivé = coût nul, pas juste coût faible.** `record()` sort avant toute allocation et
473
- avant tout appel d'horloge si le service est inactif (`auditService.ts:187`). Aucun objet créé,
473
+ avant tout appel d'horloge si le service est inactif (`auditService.ts#AuditService.record`). Aucun objet créé,
474
474
  aucun appel d'horloge. Prouvé par le banc « audit désactivé → `issueTokens` n'est pas journalisé »
475
- (`auditEmissionHotPath.test.ts:499`).
475
+ (`auditEmissionHotPath.test.ts`).
476
476
 
477
477
  **3. L'écriture ne bloque pas.** `append()` part sans `await`, avec un `.catch()` qui logge
478
- (`auditService.ts:199`). La latence du store n'entre jamais dans la latence de la requête.
478
+ (`auditService.ts#AuditService.record`). La latence du store n'entre jamais dans la latence de la requête.
479
479
 
480
480
  **4. Tout ce qui n'est pas utilisé n'est pas alloué.** La liste d'abonnés live reste `null` tant que
481
- personne n'écoute et **redevient** `null` au dernier désabonnement (`auditService.ts:228`). Le pont
481
+ personne n'écoute et **redevient** `null` au dernier désabonnement (`auditService.ts#AuditService.subscribe`). Le pont
482
482
  WS n'existe qu'entre le premier et le dernier auditeur connecté, et son tampon circulaire n'est
483
- alloué qu'au premier événement reçu (`auditBridge.ts:62`) ; son minuteur est armé à la demande et
484
- `unref` (`auditBridge.ts:93`).
483
+ alloué qu'au premier événement reçu (`auditBridge.ts#createAuditBridge`) ; son minuteur est armé à la demande et
484
+ `unref` (`auditBridge.ts#createAuditBridge`).
485
485
 
486
486
  Le pont applique en plus un **coalescing borné** : au plus une frame WS toutes les 250 ms
487
- (`auditBridge.ts:59`), tampon plafonné à 200 événements (`auditBridge.ts:60`). Sous une rafale
487
+ (`auditBridge.ts#createAuditBridge`), tampon plafonné à 200 événements (`auditBridge.ts#createAuditBridge`). Sous une rafale
488
488
  d'échecs de login, le tampon écrase les plus anciens et compte les omis dans `dropped`
489
- (`auditBridge.ts:86`) — la console affiche un récapitulatif au lieu de se figer. Superviser ne doit
489
+ (`auditBridge.ts#IAuditBatch.dropped`) — la console affiche un récapitulatif au lieu de se figer. Superviser ne doit
490
490
  jamais faire tomber ce qu'on supervise.
491
491
 
492
492
  ## ⚙️ Configuration
493
493
 
494
- Table dérivée du schéma Zod `auditSchema` (`config.ts:883`), rattaché à la racine sous la clé `audit`
495
- (`config.ts:1158`).
494
+ Table dérivée du schéma Zod `auditSchema` (`config.ts#auditSchema`), rattaché à la racine sous la clé `audit`
495
+ (`config.ts#securityConfigSchema`).
496
496
 
497
497
  | Option | Type | Défaut | Effet |
498
498
  | --------------- | --------- | -------- | ------------------------------------------------------------------------------------ |
@@ -505,20 +505,20 @@ Table dérivée du schéma Zod `auditSchema` (`config.ts:883`), rattaché à la
505
505
  > [!WARNING]
506
506
  > `immutable` et `stream` sont déclarés dans le schéma mais **ne sont lus par aucun code** aujourd'hui.
507
507
  > Les mettre à `false` ne change rien. L'immuabilité vient du **contrat** `IAuditStore`, qui n'expose
508
- > ni `update` ni `delete` ciblé (`IAuditStore.ts:48`) ; la diffusion live est gouvernée par la
509
- > **présence d'abonnés** — la liste `#listeners` du service (`auditService.ts:55`).
508
+ > ni `update` ni `delete` ciblé (`IAuditStore.ts#IAuditStore`) ; la diffusion live est gouvernée par la
509
+ > **présence d'abonnés** — la liste `#listeners` du service (`auditService.ts#AuditService.#listeners`).
510
510
 
511
511
  ### Comment `store: "auto"` décide
512
512
 
513
513
  Le défaut ne suppose rien : il **suit l'infrastructure déclarée**, borné aux backends réellement
514
- enregistrés (`auditService.ts:96`, logique `resolveAutoStore()` dans `infra.ts:289`).
514
+ enregistrés (`auditService.ts#AuditService.#build`, logique `resolveAutoStore()` dans `infra.ts#resolveAutoStore`).
515
515
 
516
516
  1. `NF_STORE` posée et le backend est enregistré pour l'audit → il gagne (levier de banc de charge) ;
517
517
  2. sinon, une base est déclarée (`NF_DATABASE_URL`) → `drizzle`, ou `mongoose` selon la famille ;
518
518
  3. sinon, un backend local persistant réellement chargé (`drizzle` = SQLite, puis `mongoose`) ;
519
519
  4. sinon, repli **annoncé** sur `memory` (volatil) — la décision est loggée en INFO, jamais silencieuse.
520
520
 
521
- La résolution finale est publiée au kernel (`auditService.ts:145`), ce qui alimente l'écran des stores
521
+ La résolution finale est publiée au kernel (`auditService.ts#AuditService.#build`), ce qui alimente l'écran des stores
522
522
  de Studio : configuré, résolu, disponible, motif, emplacement physique.
523
523
 
524
524
  ## 🏗️ Architecture interne
@@ -542,39 +542,39 @@ sequenceDiagram
542
542
 
543
543
  Quatre pièces, quatre responsabilités :
544
544
 
545
- - **`recordAudit()`** (`recordAudit.ts:15`) — le point d'émission côté appelant. Une résolution par le
545
+ - **`recordAudit()`** (`recordAudit.ts#recordAudit`) — le point d'émission côté appelant. Une résolution par le
546
546
  container, sur le cold-path uniquement. C'est ce qui rend l'audit **découplé** : module absent →
547
547
  aucun effet, jamais d'exception qui remonterait dans le flux métier.
548
- - **`AuditService`** (`auditService.ts:51`) — le propriétaire. Il construit le store au boot, le pose
549
- au container sous le nom `auditStore` (`auditService.ts:144`), arme la purge, et implémente
550
- `IAuditSink` (`IAuditStore.ts:77`).
551
- - **`IAuditStore`** (`IAuditStore.ts:48`) — le contrat de persistance : `append`, `listPage`, `gc`.
548
+ - **`AuditService`** (`auditService.ts#AuditService.#store`) — le propriétaire. Il construit le store au boot, le pose
549
+ au container sous le nom `auditStore` (`auditService.ts#AuditService.#build`), arme la purge, et implémente
550
+ `IAuditSink` (`IAuditStore.ts#IAuditSink`).
551
+ - **`IAuditStore`** (`IAuditStore.ts#IAuditStore`) — le contrat de persistance : `append`, `listPage`, `gc`.
552
552
  **`append` est la seule écriture** : ni `update`, ni `delete` ciblé. L'immuabilité EST la garantie
553
553
  d'audit.
554
- - **`createAuditBridge()`** (`auditBridge.ts:53`) — le pont vers le canal WS `nodefony:audit`
555
- (`auditBridge.ts:8`), enregistré comme canal **système** par le firewall (`firewall.ts:387`) et donc
556
- gardé par le plancher `security:` → `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts:106`).
554
+ - **`createAuditBridge()`** (`auditBridge.ts#createAuditBridge`) — le pont vers le canal WS `nodefony:audit`
555
+ (`auditBridge.ts#SECURITY_AUDIT_CHANNEL`), enregistré comme canal **système** par le firewall (`firewall.ts#Firewall.#wireRealtime`) et donc
556
+ gardé par le plancher `security:` → `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts#DEFAULT_SYSTEM_PREFIXES`).
557
557
 
558
558
  ### La lecture paginée — pourquoi un curseur et pas un décalage
559
559
 
560
560
  Un journal reçoit des écritures **pendant** qu'on le parcourt. Un `OFFSET 200` glisserait d'une page
561
561
  à l'autre : on reverrait des événements déjà lus, on en manquerait d'autres. Le contrat impose donc le
562
- **curseur** (`IAuditStore.ts:22`).
562
+ **curseur** (`IAuditStore.ts#IAuditListQuery`).
563
563
 
564
564
  Le curseur est **composite et auto-portant** : `<ts>:<id>`. Deux propriétés que les deux backends
565
565
  tiennent identiquement :
566
566
 
567
567
  - **ordre total** — l'horodatage porte la chronologie, l'identifiant départage les collisions à la
568
568
  milliseconde. Sans ce tri `desc(ts), desc(id)`, trois échecs de login de la même milliseconde
569
- pourraient se répéter ou disparaître (`MemoryAuditStore.ts:83`, `DrizzleAuditStore.ts:197`) ;
569
+ pourraient se répéter ou disparaître (`MemoryAuditStore.ts#MemoryAuditStore.listPage`, `DrizzleAuditStore.ts#DrizzleAuditStore.listPage`) ;
570
570
  - **auto-portance** — le jeton contient tout ce qu'il faut pour se comparer, donc une page reste juste
571
- même si l'événement qui l'a produite a été purgé entre-temps (`MemoryAuditStore.ts:115`,
572
- `DrizzleAuditStore.ts:219`). Un curseur réduit à un identifiant nu aurait rembobiné en silence à la
571
+ même si l'événement qui l'a produite a été purgé entre-temps (`MemoryAuditStore.ts#MemoryAuditStore.#parseCursor`,
572
+ `DrizzleAuditStore.ts#DrizzleAuditStore.listPage`). Un curseur réduit à un identifiant nu aurait rembobiné en silence à la
573
573
  première page — et fait boucler la console.
574
574
 
575
575
  Le `total` est **refusable** (`withTotal: false`) : un `COUNT` filtré sur une rétention longue se
576
- paie (`IAuditStore.ts:59`). `hasNext` et `nextCursor` restent fiables dans les deux cas, grâce à une
577
- ligne de garde `limit + 1` (`DrizzleAuditStore.ts:225`).
576
+ paie (`IAuditStore.ts#IAuditStore.listPage`). `hasNext` et `nextCursor` restent fiables dans les deux cas, grâce à une
577
+ ligne de garde `limit + 1` (`DrizzleAuditStore.ts#DrizzleAuditStore.listPage`).
578
578
 
579
579
  ## Entité de persistance et dialectes pris en charge
580
580
 
@@ -599,9 +599,9 @@ garder tout ça en mémoire vive coûte cher pour un motif d'accès qui n'est pa
599
599
 
600
600
  ### La table `audit_event`
601
601
 
602
- L'entité est déclarée en spécification logique (`auditEventEntity.ts:37`), déclinée par dialecte via
602
+ L'entité est déclarée en spécification logique (`auditEventEntity.ts#auditEventSchema`), déclinée par dialecte via
603
603
  le `colKit` — mêmes **noms** de colonnes partout, donc un store dialect-agnostique. La forme de ligne
604
- `AuditEventRow` (`auditEventEntity.ts:85`) est **identique** au contrat `IAuditEvent`, champ pour
604
+ `AuditEventRow` (`auditEventEntity.ts#AuditEventRow`) est **identique** au contrat `IAuditEvent`, champ pour
605
605
  champ : aucun mapping surprise.
606
606
 
607
607
  | Colonne | SQLite | PostgreSQL | MySQL/MariaDB | Note |
@@ -618,37 +618,37 @@ En MySQL, toute colonne texte **indexée** (ou clé primaire) devient `varchar(5
618
618
  pas indexer un `TEXT` sans longueur de préfixe. Même règle pour tout le framework, portée par le
619
619
  `colKit` — jamais par l'entité.
620
620
 
621
- Quatre index couvrent les axes de la console (`auditEventEntity.ts:61`) : `ts` pour la pagination
621
+ Quatre index couvrent les axes de la console (`auditEventEntity.ts#AuditEventRow`) : `ts` pour la pagination
622
622
  chronologique, `category`, `actor` et `requestId` pour les filtres. Ils sont lus par `drizzle-kit`
623
623
  pour les migrations de production ; en dev et en test, le DDL dérivé les ignore — pure performance de
624
624
  filtrage, jamais de sémantique.
625
625
 
626
626
  L'entité et la fabrique sont enregistrées automatiquement par l'adapter au démarrage
627
- (`registerStores.ts:276`, entité via `registerAuditEntities()`, `auditEventEntity.ts:141`). Côté
628
- implémentation, `DrizzleAuditStore` (`DrizzleAuditStore.ts:64`) résout son handle de base **à chaque
627
+ (`registerStores.ts#registerMongooseFrameworkStores`, entité via `registerAuditEntities()`, `auditEventEntity.ts#registerAuditEntities`). Côté
628
+ implémentation, `DrizzleAuditStore` (`DrizzleAuditStore.ts#DrizzleAuditStore`) résout son handle de base **à chaque
629
629
  appel**, pas à la construction : l'ordre de démarrage n'est pas garanti, et l'ORM se déconnecte au
630
630
  `onTerminate` avant le drain des serveurs.
631
631
 
632
632
  ## 🔐 Sécurité — ce qui n'entre JAMAIS dans le journal
633
633
 
634
- La règle d'or est écrite en tête du contrat (`IAuditEvent.ts:8`) : **un secret n'entre jamais dans un
634
+ La règle d'or est écrite en tête du contrat (`IAuditEvent.ts#AuditCategory`) : **un secret n'entre jamais dans un
635
635
  événement**. Ni mot de passe, ni jeton, ni cookie, ni corps de requête, ni en-têtes.
636
636
 
637
637
  Ce que le code garantit, concrètement :
638
638
 
639
639
  - **Le typage rend le secret difficile à faire entrer.** `actor` est documenté comme un _libellé
640
- d'identité_ (`IAuditEvent.ts:82`) et `resource` comme un _descripteur léger_ — « jamais le corps ni
641
- les en-têtes de la requête » (`IAuditEvent.ts:87`).
642
- - **La présence remplace la valeur.** `IAuditEventFlags` (`IAuditEvent.ts:53`) ne porte que deux
640
+ d'identité_ (`IAuditEvent.ts#IAuditEvent.actor`) et `resource` comme un _descripteur léger_ — « jamais le corps ni
641
+ les en-têtes de la requête » (`IAuditEvent.ts#IAuditEvent.resource`).
642
+ - **La présence remplace la valeur.** `IAuditEventFlags` (`IAuditEvent.ts#IAuditEventFlags`) ne porte que deux
643
643
  booléens : un en-tête `Authorization` était-il là, un cookie était-il là. `readAuditContext()`
644
- les calcule par un simple `Boolean(headers[…])` (`readAuditContext.ts:40`) — la valeur n'est jamais
644
+ les calcule par un simple `Boolean(headers[…])` (`readAuditContext.ts#readAuditContext`) — la valeur n'est jamais
645
645
  copiée.
646
646
  - **Les émetteurs journalisent l'identifiant public, pas le secret.** Une clé d'API émet son `id`
647
647
  public en `resource`, jamais le jeton. Prouvé explicitement : le test vérifie que la sérialisation
648
648
  complète de l'événement **ne contient pas** le secret créé
649
- (`auditEmissionHotPath.test.ts:545`).
649
+ (`auditEmissionHotPath.test.ts`).
650
650
  - **Le motif reste machine.** `reason` est une valeur stable et filtrable, pas un message d'erreur
651
- libre (`IAuditEvent.ts:92`) : c'est ce qui empêche une cause fine de fuir dans la trace… et qui
651
+ libre (`IAuditEvent.ts#IAuditEvent.reason`) : c'est ce qui empêche une cause fine de fuir dans la trace… et qui
652
652
  permet de la garder **côté audit** alors que le client, lui, reçoit un message d'échec uniforme
653
653
  (anti-énumération).
654
654
 
@@ -656,62 +656,62 @@ Deux autres propriétés de sécurité valent d'être connues :
656
656
 
657
657
  - **Pas d'événement fantôme.** Révoquer la clé d'API d'autrui n'émet **rien** : l'anti-énumération
658
658
  vaut aussi pour le journal, sinon le journal lui-même deviendrait un oracle
659
- (`auditEmissionHotPath.test.ts:561`).
659
+ (`auditEmissionHotPath.test.ts`).
660
660
  - **Lecture réservée aux administrateurs.** L'endpoint est gardé `ROLE_NODEFONY_ADMIN`, le canal live
661
- aussi via le plancher irréductible `security:` (`frameAuthorizer.ts:107`) — un utilisateur ordinaire
661
+ aussi via le plancher irréductible `security:` (`frameAuthorizer.ts#SECURITY_CHANNEL_POLICY`) — un utilisateur ordinaire
662
662
  qui tente de s'y abonner produit lui-même un `frame.denied`.
663
663
 
664
664
  ## 📜 Normes appliquées
665
665
 
666
- | Exigence | Norme | Comment le code s'y conforme |
667
- | ----------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------- |
668
- | Journaliser les échecs d'authentification | OWASP Logging Cheat Sheet | `auth.failure`/`auth.throttled`/`auth.denied` (`firewall.ts:693`) |
669
- | Journaliser les refus d'autorisation | OWASP A09:2021 | `access.denied` sur tout refus du jury (`authorization.ts:130`) |
670
- | Ne jamais journaliser de secret | OWASP Logging Cheat Sheet | flags de **présence** seuls (`IAuditEvent.ts:49`) |
671
- | Traçabilité « qui, quoi, quand, d'où » | ISO 27001 A.8.15 (journalisation) | acteur, action, horodatage et provenance dans `IAuditEvent` (`IAuditEvent.ts:61`) |
672
- | Journal inaltérable | ISO 27001 A.8.15 | contrat append-only, aucune mutation exposée (`IAuditStore.ts:48`) |
673
- | Rétention bornée / minimisation | RGPD art. 5.1.e | purge par âge pilotée par `retentionDays` (`config.ts:912`) |
674
- | Détection de rejeu de jeton | RFC 9700 §4.14 | `token.reuse_detected` + coupure de famille (`tokenService.ts:585`) |
675
- | Backoff de login journalisé | NIST SP 800-63B | `auth.throttled` avec `reason: "throttled"` (`firewall.ts:773`) |
666
+ | Exigence | Norme | Comment le code s'y conforme |
667
+ | ----------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------- |
668
+ | Journaliser les échecs d'authentification | OWASP Logging Cheat Sheet | `auth.failure`/`auth.throttled`/`auth.denied` (`firewall.ts#Firewall.describeRoleHierarchy`) |
669
+ | Journaliser les refus d'autorisation | OWASP A09:2021 | `access.denied` sur tout refus du jury (`authorization.ts#Authorization.#auditDeny`) |
670
+ | Ne jamais journaliser de secret | OWASP Logging Cheat Sheet | flags de **présence** seuls (`IAuditEvent.ts#AuditOutcome`) |
671
+ | Traçabilité « qui, quoi, quand, d'où » | ISO 27001 A.8.15 (journalisation) | acteur, action, horodatage et provenance dans `IAuditEvent` (`IAuditEvent.ts#IAuditEvent`) |
672
+ | Journal inaltérable | ISO 27001 A.8.15 | contrat append-only, aucune mutation exposée (`IAuditStore.ts#IAuditStore`) |
673
+ | Rétention bornée / minimisation | RGPD art. 5.1.e | purge par âge pilotée par `retentionDays` (`config.ts#auditSchema`) |
674
+ | Détection de rejeu de jeton | RFC 9700 §4.14 | `token.reuse_detected` + coupure de famille (`tokenService.ts#TokenService.issueTokens`) |
675
+ | Backoff de login journalisé | NIST SP 800-63B | `auth.throttled` avec `reason: "throttled"` (`firewall.ts#Firewall.handleSecurity`) |
676
676
 
677
677
  ## 📡 Observabilité — Studio
678
678
 
679
- L'écran **Sécurité → Journal d'audit** (`/nodefony/audit`) (`Audit.tsx:62`) consomme le data plane
679
+ L'écran **Sécurité → Journal d'audit** (`/nodefony/audit`) (`Audit.tsx#ENDPOINT`) consomme le data plane
680
680
  `GET /nodefony/security/api/audit/events` en pagination **serveur**. Il offre :
681
681
 
682
- - un tableau filtrable par heure, catégorie, action, issue, acteur, raison et IP (`Audit.tsx:227`) ;
682
+ - un tableau filtrable par heure, catégorie, action, issue, acteur, raison et IP (`Audit.tsx#Audit`) ;
683
683
  - un indicateur du **store réellement résolu** pour la brique `audit`, alimenté par la publication de
684
- résolution du service (`auditService.ts:145`) ;
685
- - un interrupteur **Temps réel** qui s'abonne au canal `nodefony:audit` (`AuditLive.tsx:20`) — pensé
684
+ résolution du service (`auditService.ts#AuditService.#build`) ;
685
+ - un interrupteur **Temps réel** qui s'abonne au canal `nodefony:audit` (`AuditLive.tsx#AuditLive`) — pensé
686
686
  pour les pics d'activité : un journal d'audit se consulte, il ne se regarde pas défiler ;
687
687
  - un renvoi vers la **trace de requête** quand l'événement porte un `requestId`, ce qui recolle
688
688
  l'événement de sécurité à toute la vie de la requête.
689
689
 
690
690
  Quand l'audit est désactivé en configuration, l'endpoint répond **503** avec un message explicite
691
- (`SecurityAdminApi.ts:341`) et la console l'affiche tel quel plutôt qu'un tableau vide ambigu.
691
+ (`SecurityAdminApi.ts#createSecurityAdminApi`) et la console l'affiche tel quel plutôt qu'un tableau vide ambigu.
692
692
 
693
693
  ### Le journal alimente les webhooks
694
694
 
695
695
  Le flux d'audit est la **source d'événements** du dispatcher de webhooks sortants : celui-ci s'abonne
696
- au service au démarrage (`webhooks.ts:214`) et filtre les actions souscrites avant de livrer
697
- (`WebhookDispatcher.ts:132`). Conséquence pratique : ce qui n'est pas audité ne peut pas déclencher de
696
+ au service au démarrage (`webhooks.ts#WebhookService.#attachDispatcher`) et filtre les actions souscrites avant de livrer
697
+ (`WebhookDispatcher.ts#WebhookDispatcher.onAuditEvent`). Conséquence pratique : ce qui n'est pas audité ne peut pas déclencher de
698
698
  webhook. Détail des souscriptions, de la signature et des relivraisons → [webhooks](webhooks.md).
699
699
 
700
700
  ## ⚠️ Pièges
701
701
 
702
- | Symptôme | Cause (dans le code) | Correction |
703
- | --------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------- |
704
- | Journal vide après un redémarrage | Store `memory` : volatile et per-pod (`MemoryAuditStore.ts:37`) | `audit.store: "drizzle"` (ou déclarer `NF_DATABASE_URL`) |
705
- | Un pod voit des événements, l'autre non | Store `memory` non partagé | Store durable partagé |
706
- | Le boot échoue en production sur l'audit | Store **explicite** inconnu, fail-closed (`auditService.ts:115`) | Corriger le nom, ou charger l'adapter qui l'enregistre |
707
- | `limit=5000` ne rend que 200 événements | Plafond de l'endpoint (`parsePageQuery`, `pageQuery.ts:77`) | Paginer avec `nextCursor`, jamais gonfler `limit` |
708
- | La page 2 répète ou saute des événements | Pagination réimplémentée en offset | Repasser le `nextCursor` reçu — le curseur est opaque |
709
- | `?category=authen` renvoie 400 | Valeur hors union refusée (`PageQueryError`) | Utiliser une valeur de l'union (`auth`, `authz`, `token`…) |
710
- | Le paramètre `q` renvoie 400 | Recherche plein texte non supportée sur ce journal | Filtrer par `category`/`actor`/`action`/`requestId` |
711
- | `stream: false` ne coupe pas le live | Drapeau non lu ; le live suit `#listeners` (`auditService.ts:55`) | Retirer le rôle admin, ou ne pas exposer le canal |
712
- | Trous dans le journal pendant une panne de base | Écriture best-effort (`auditService.ts:199`) | Store à haute disponibilité si la conformité l'exige |
713
- | Aucun `login.success` alors que les logins marchent | Le succès du **firewall** est muet ; `login.success` vient du BFF | Filtrer `action=login.success`, pas `category=auth` seul |
714
- | Endpoint d'audit en 503 | `audit.enabled: false` (`SecurityAdminApi.ts:320`) | Réactiver l'audit en configuration |
702
+ | Symptôme | Cause (dans le code) | Correction |
703
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
704
+ | Journal vide après un redémarrage | Store `memory` : volatile et per-pod (`MemoryAuditStore.ts#MemoryAuditStore`) | `audit.store: "drizzle"` (ou déclarer `NF_DATABASE_URL`) |
705
+ | Un pod voit des événements, l'autre non | Store `memory` non partagé | Store durable partagé |
706
+ | Le boot échoue en production sur l'audit | Store **explicite** inconnu, fail-closed (`auditService.ts#AuditService.#build`) | Corriger le nom, ou charger l'adapter qui l'enregistre |
707
+ | `limit=5000` ne rend que 200 événements | Plafond de l'endpoint (`parsePageQuery`, `pageQuery.ts#toPageParams`) | Paginer avec `nextCursor`, jamais gonfler `limit` |
708
+ | La page 2 répète ou saute des événements | Pagination réimplémentée en offset | Repasser le `nextCursor` reçu — le curseur est opaque |
709
+ | `?category=authen` renvoie 400 | Valeur hors union refusée (`PageQueryError`) | Utiliser une valeur de l'union (`auth`, `authz`, `token`…) |
710
+ | Le paramètre `q` renvoie 400 | Recherche plein texte non supportée sur ce journal | Filtrer par `category`/`actor`/`action`/`requestId` |
711
+ | `stream: false` ne coupe pas le live | Drapeau non lu ; le live suit `#listeners` (`auditService.ts#AuditService.#listeners`) | Retirer le rôle admin, ou ne pas exposer le canal |
712
+ | Trous dans le journal pendant une panne de base | Écriture best-effort (`auditService.ts#AuditService.record`) | Store à haute disponibilité si la conformité l'exige |
713
+ | Aucun `login.success` alors que les logins marchent | Le succès du **firewall** est muet ; `login.success` vient du BFF | Filtrer `action=login.success`, pas `category=auth` seul |
714
+ | Endpoint d'audit en 503 | `audit.enabled: false` (`SecurityAdminApi.ts#createSecurityAdminApi`) | Réactiver l'audit en configuration |
715
715
 
716
716
  ## 🧪 Tests & couverture
717
717