@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
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # @nodefony/security
2
+
3
+ Couche de sécurité de Nodefony.
4
+
5
+ Firewall par zones, authentication (pattern `IAuthenticator`), autorisation (rôles + voters),
6
+ CORS, CSRF, en-têtes de sécurité, JWT, clés API, WebAuthn/passkeys, OAuth2 social, 2FA TOTP,
7
+ webhooks signés, audit persistant. **Modèle d'identité hybride**, **Zero Trust** par défaut.
8
+ Consomme [`@nodefony/user`](https://www.npmjs.com/package/@nodefony/user).
9
+
10
+ > **Statut** : cœur livré — firewall + zones, session serveur (NIST), JWT (`jose`), WebAuthn,
11
+ > OAuth2 social, CSRF/CORS/en-têtes natifs, clés API/PAT, 2FA TOTP, webhooks, audit persistant,
12
+ > rate-limit, **voters d'autorisation** (`RoleVoter` + `ScopeVoter`, jury affirmatif à véto
13
+ > `DENY`, découverts au boot par le `voterRegistry`) et les décorateurs **`@IsGranted`** et
14
+ > **`@CsrfProtect`**, appliqués par le `Resolver` avant l'action.
15
+ >
16
+ > Reste : ACL fine par ressource (niveau B de `authorization.ts`), journal d'authentification
17
+ > dédié, `rpId` WebAuthn dérivé du `Host` (multi-vhost), `MTlsAuthenticator` (niche). Le serveur
18
+ > d'autorisation OAuth 2.1 est tranché **après** la 10.0.0.
19
+
20
+ ## Principes
21
+
22
+ - **Pattern `IAuthenticator`** (supports / authenticate / onSuccess) — pas de Bridge/Factory.
23
+ - **Zero Trust** : une zone protégée sans utilisateur authentifié → `401`.
24
+ - **Identité hybride** (révisé 2026-06-06) : le **web/Studio** ouvre une **session serveur**
25
+ (cookie opaque BFF, révocable, `HttpOnly; Secure; SameSite`) ; les **API/agents** portent leur
26
+ preuve à chaque requête (**JWT** signé / clé API). Jamais « full stateless » : la session reste
27
+ la fondation web (révocation immédiate), le JWT est réservé au sans-état machine-à-machine.
28
+ - **Config type-safe** : schéma **Zod** (18 sections, tout `enabled`), **introspectable** (Studio
29
+ génère son formulaire d'édition). L'app configure via `use("@nodefony/security", { … })`.
30
+ - **En-têtes natifs** (sans la lib `helmet`) — 0 dépendance, nonce CSP par requête.
31
+
32
+ ## Configuration
33
+
34
+ La sécurité se configure dans le **`nodefony.config.ts`** de l'app via `use()` — colocalisée
35
+ avec le chargement du module, jamais dans un fichier séparé :
36
+
37
+ ```ts
38
+ // nodefony.config.ts
39
+ import { defineConfig, use } from "nodefony";
40
+
41
+ export default defineConfig((ctx) => ({
42
+ modules: [
43
+ "@nodefony/http",
44
+ "@nodefony/framework",
45
+ use(
46
+ "@nodefony/security",
47
+ {
48
+ // Hiérarchie de rôles (RBAC) — ROLE_X hérite des rôles listés (DFS au boot).
49
+ roleHierarchy: { ROLE_ADMIN: ["ROLE_USER"] },
50
+ // Zones firewall — clé = nom de zone, valeur = pattern + authenticators.
51
+ areas: {
52
+ // Web/Studio : session serveur (cookie opaque BFF).
53
+ admin: { pattern: "^/admin", authenticators: ["session"] },
54
+ // API machine-à-machine : preuve à chaque requête, aucune session.
55
+ "api-m2m": {
56
+ pattern: "^/api",
57
+ authenticators: ["jwt", "apikey"],
58
+ stateless: true,
59
+ },
60
+ },
61
+ },
62
+ { policy: "mandatory" }, // sécurité = requise dès qu'on sert du trafic
63
+ ),
64
+ ],
65
+ }));
66
+ ```
67
+
68
+ > Une zone se déclare idéalement **par module** (override `module-security`, dans la config du
69
+ > module) pour vivre au plus près de ses routes. Authenticators fournis : `anonymous`, `session`,
70
+ > `userpassword`, `jwt`, `apikey`, `webauthn`, plus les providers OAuth2. La chaîne est validée
71
+ > au boot (`mode: "first"` par défaut = le premier qui reconnaît authentifie ; `"all"` = tous
72
+ > requis, ex. mTLS + JWT). Config invalide → firewall **fail-closed** (tout rejeté).
73
+
74
+ Toutes les sections (`cors`, `csrf`, `headers`, `rateLimit`, `jwt`, `apiKeys`, `webhooks`,
75
+ `audit`, `studio`…) ont des **défauts sûrs** et sont **désactivables** via `enabled`.
76
+ Voir [`nodefony/config/config.ts`](https://github.com/nodefony/nodefony-core/blob/claude-ts/src/packages/@nodefony/security/nodefony/config/config.ts) — chaque option y est
77
+ documentée (explication + défaut + reco).
78
+
79
+ ### Sections
80
+
81
+ <!-- prettier-ignore -->
82
+ | Section | Rôle | Défaut |
83
+ | --- | --- | --- |
84
+ | `encoders` | hash mot de passe | Argon2id (OWASP) ; bcrypt legacy |
85
+ | `roleHierarchy` | héritage de rôles | `{}` (plats) |
86
+ | `areas` | zones firewall (pattern + host + authenticators) | `{}` (aucune route protégée) |
87
+ | `cors` | Cross-Origin | strict (jamais `*`+credentials) |
88
+ | `csrf` | Fetch Metadata (`Sec-Fetch-Site`) + repli Origin (OWASP 2025) | activé ; `strictSameSite:false`, `trustedOrigins:[]` |
89
+ | `headers` | HSTS/CSP+nonces/frameguard/noSniff… (natif) | activé ; avancés (COOP/COEP/CORP…) en option |
90
+ | `rateLimit` | anti brute-force + lockout | activé |
91
+ | `jwt` | jetons API/agents (sans-état) | EdDSA, access 15 min / refresh 7 j, rotation |
92
+ | `apiKeys` | clés API (PAT) hashées | préfixe `nf`, expiry 90 j |
93
+ | `webhooks` | sortants signés HMAC | anti-replay + anti-SSRF |
94
+ | `audit` | journal sécurité (append-only) | activé, stream Studio |
95
+ | `studio` | durcissement console admin | **OFF**, `localhost` (durcissement réservé) |
96
+
97
+ ### CSRF — Fetch Metadata d'abord
98
+
99
+ La défense CSRF est **globale** (toute requête qui modifie l'état : `POST`/`PUT`/`PATCH`/`DELETE` ;
100
+ les méthodes sûres `GET`/`HEAD`/`OPTIONS` ne sont jamais bloquées). Trois couches, dans l'ordre :
101
+
102
+ 1. **`Sec-Fetch-Site`** (défense primaire) — le navigateur tamponne lui-même la provenance de la
103
+ requête, un script attaquant ne peut pas la falsifier. `same-origin` et `none` (navigation directe
104
+ ou client non-navigateur) passent ; `cross-site` est **rejeté en 403**.
105
+ 2. **Repli `Origin`/`Referer`** — pour les vieux navigateurs sans `Sec-Fetch-*` : l'origine doit
106
+ correspondre à l'hôte de l'app. Ni l'un ni l'autre (client non-navigateur) → autorisé (hors vecteur).
107
+ 3. **Cookie `SameSite=Lax`** — défense en profondeur sur le cookie de session.
108
+
109
+ ```ts
110
+ csrf: {
111
+ strictSameSite: false, // true → bloque aussi les sous-domaines (same-site) : multi-tenant
112
+ trustedOrigins: ["https://app.example.org"], // alias multi-domaine légitimes (autorisés même cross-site)
113
+ }
114
+ ```
115
+
116
+ > `trustedOrigins` est **distinct** de `cors.origins` : déclarer un simple alias de domaine ne doit pas
117
+ > ouvrir la lecture CORS des réponses au JavaScript tiers. Une origine listée dans `cors.origins` est
118
+ > toutefois aussi acceptée (ce que CORS autorise explicitement n'est pas du CSRF).
119
+
120
+ Le token synchronizer renforcé (`@CsrfProtect` / `@CsrfExempt`) arrive à l'étape suivante.
121
+
122
+ ### CORS — l'inverse du CSRF
123
+
124
+ CORS **assouplit** la Same-Origin Policy : il autorise un site tiers à _lire_ la réponse de l'app en
125
+ JavaScript (le CSRF, lui, _empêche_ un tiers de déclencher une mutation). La politique est globale :
126
+
127
+ - **Preflight** `OPTIONS` (Fetch Standard) court-circuité **avant le routing** → `204` + en-têtes
128
+ `Access-Control-Allow-*`. Il ne s'authentifie jamais (il ne porte pas de credentials).
129
+ - **Origine autorisée** → l'origine est **reflétée** (`Access-Control-Allow-Origin: <origine>` + `Vary: Origin`).
130
+ `*` n'est émis que sans `credentials`. Origine non autorisée → aucun en-tête (le navigateur bloque).
131
+ - **`origins:["*"]` + `credentials:true` est refusé au démarrage** (refine Zod) : le navigateur l'interdit,
132
+ et c'est une faille classique. Pour les credentials, lister les origines explicitement.
133
+
134
+ ### Introspection (Studio)
135
+
136
+ ```ts
137
+ import { securityConfigJsonSchema } from "@nodefony/security";
138
+ const schema = securityConfigJsonSchema(); // JSON Schema → formulaire d'édition Studio
139
+ ```
140
+
141
+ ## Journal d'audit (événements de sécurité)
142
+
143
+ Le journal trace les **transitions d'état** de sécurité (login, refus, jeton émis/révoqué, verrou
144
+ WS) — distinct du log de trafic (1 PDU/requête). Émission **explicite** par point sensible ; le
145
+ **chemin de succès reste muet** (le volume n'est pas un signal), seul l'échec/refus émet (cold-path)
146
+ → aucun coût ajouté au hot-path nominal. Activé par défaut (`audit.enabled`, OWASP A09), coût nul si
147
+ désactivé. Lecture : `GET /nodefony/security/api/audit/events` (RBAC `ROLE_NODEFONY_ADMIN`).
148
+
149
+ **Flux temps réel** (lot 4) : canal WS **`nodefony:audit`**, réservé `ROLE_NODEFONY_ADMIN`
150
+ (plancher système, un cran au-dessus de l'observabilité générique `ROLE_ADMIN`). Enregistré comme
151
+ **canal système** sur le hub realtime (servable par tout endpoint, sans couplage à Studio) ;
152
+ **lazy** : le pont ne s'abonne au journal qu'au 1ᵉʳ auditeur connecté et s'en détache au dernier.
153
+ Coalescé (1 frame `{ events, dropped }` toutes les ~250 ms, ring borné) → un pic d'échecs sous
154
+ attaque ne noie pas la console. Un user non habilité qui tente de s'y abonner est refusé **et audité**
155
+ (`frame.denied`).
156
+
157
+ Un **secret n'entre jamais** dans un événement — seule sa _présence_ est tracée (`flags`).
158
+
159
+ | `action` | `category` | `outcome` | Émis par |
160
+ | ---------------------- | ---------- | --------- | -------------------------------- |
161
+ | `login.success` | `auth` | success | `AuthFlow` (BFF) / fédéré |
162
+ | `login.failure` | `auth` | failure | `AuthFlow`, `TokenService` grant |
163
+ | `login.throttled` | `auth` | failure | `AuthFlow`, `TokenService` grant |
164
+ | `logout` | `session` | success | `AuthFlow` |
165
+ | `auth.failure` | `auth` | failure | `Firewall` (credential invalide) |
166
+ | `auth.throttled` | `auth` | failure | `Firewall` (backoff NIST) |
167
+ | `auth.denied` | `auth` | denied | `Firewall` (Zero Trust) |
168
+ | `access.denied` | `authz` | denied | `Authorization` (voters/RBAC) |
169
+ | `frame.denied` | `ws` | denied | verrou de frame WS |
170
+ | `token.issued` | `token` | success | `TokenService` |
171
+ | `token.reuse_detected` | `token` | denied | `TokenService` (RFC 9700) |
172
+ | `apikey.created` | `token` | success | `ApiKeyService` |
173
+ | `apikey.revoked` | `token` | success | `ApiKeyService` |
174
+
175
+ ## Erreurs
176
+
177
+ - `AuthenticationError` → `401` (non authentifié).
178
+ - `AccessDeniedError` → `403` (authentifié mais non autorisé).
179
+
180
+ ## Licence
181
+
182
+ CeCILL-B — Christophe CAMENSULI.
@@ -0,0 +1,9 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+ //#endregion
9
+ export { __decorate as default };
@@ -0,0 +1,6 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
2
+ function __decorateMetadata(k, v) {
3
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
+ }
5
+ //#endregion
6
+ export { __decorateMetadata as default };
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ import config from "./nodefony/config/config.js";
2
+ import { SecuredArea } from "./nodefony/src/SecuredArea.js";
3
+ import { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
4
+ import { RoleHierarchyWalker } from "./nodefony/src/RoleHierarchyWalker.js";
5
+ import { CsrfTokenManager } from "./nodefony/src/csrfToken.js";
6
+ import { CsrfError } from "./nodefony/errors/CsrfError.js";
7
+ import { Csrf } from "./nodefony/service/csrf.js";
8
+ import { Cors } from "./nodefony/service/cors.js";
9
+ import { SecurityHeaders } from "./nodefony/service/securityHeaders.js";
10
+ import { AuthenticationError } from "./nodefony/errors/AuthenticationError.js";
11
+ import { ThrottledError } from "./nodefony/errors/ThrottledError.js";
12
+ import { UnverifiableTokenError } from "./nodefony/errors/UnverifiableTokenError.js";
13
+ import { defineSecurityConfig, securityConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
14
+ import { AnonymousToken } from "./nodefony/src/token/AnonymousToken.js";
15
+ import { AnonymousAuthenticator } from "./nodefony/src/authenticator/AnonymousAuthenticator.js";
16
+ import { UserToken } from "./nodefony/src/token/UserToken.js";
17
+ import { SessionAuthenticator } from "./nodefony/src/authenticator/SessionAuthenticator.js";
18
+ import { UserPasswordAuthenticator } from "./nodefony/src/authenticator/UserPasswordAuthenticator.js";
19
+ import peekIssuer from "./nodefony/src/authenticator/peekIssuer.js";
20
+ import { JwtAuthenticator } from "./nodefony/src/authenticator/JwtAuthenticator.js";
21
+ import { generateApiKey, hashApiKey, looksLikeApiKey, parseApiKey } from "./nodefony/src/apikey/apiKeyFormat.js";
22
+ import { ApiKeyAuthenticator } from "./nodefony/src/authenticator/ApiKeyAuthenticator.js";
23
+ import { ExternalJwtAuthenticator } from "./nodefony/src/authenticator/ExternalJwtAuthenticator.js";
24
+ import { resolveJwtRuntime } from "./nodefony/src/token/jwtRuntime.js";
25
+ import { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthenticatorFactory } from "./nodefony/src/authenticator/authenticatorRegistry.js";
26
+ import { recordAudit } from "./nodefony/src/audit/recordAudit.js";
27
+ import { readAuditContext } from "./nodefony/src/audit/readAuditContext.js";
28
+ import { Firewall } from "./nodefony/service/firewall.js";
29
+ import { AuthFlow } from "./nodefony/service/authFlow.js";
30
+ import { InvalidTargetError } from "./nodefony/errors/InvalidTargetError.js";
31
+ import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./nodefony/src/token/tokenSort.js";
32
+ import { matchesTokenStatus, tokenStatusOf } from "./nodefony/src/token/tokenStatus.js";
33
+ import { MemoryTokenStore } from "./nodefony/src/token/MemoryTokenStore.js";
34
+ import { getTokenStoreFactory, listTokenStores, registerTokenStore } from "./nodefony/src/token/tokenStoreRegistry.js";
35
+ import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
36
+ import { TokenService } from "./nodefony/service/tokenService.js";
37
+ import { RemoteJwtVerifier } from "./nodefony/src/token/RemoteJwtVerifier.js";
38
+ import { AccessTokenVerifierService } from "./nodefony/service/accessTokenVerifier.js";
39
+ import { VoterVote } from "./nodefony/contracts/IAccessVoter.js";
40
+ import { RoleVoter } from "./nodefony/src/voter/RoleVoter.js";
41
+ import { ScopeVoter } from "./nodefony/src/voter/ScopeVoter.js";
42
+ import { listVoterFactories, registerVoterFactory } from "./nodefony/src/voter/voterRegistry.js";
43
+ import { Authorization } from "./nodefony/service/authorization.js";
44
+ import { MemoryWebAuthnCredentialStore } from "./nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js";
45
+ import { getWebAuthnStoreFactory, listWebAuthnStores, registerWebAuthnStore } from "./nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js";
46
+ import { WebAuthnService } from "./nodefony/service/webAuthn.js";
47
+ import { getOAuthProviderFactory, listOAuthProviders, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
48
+ import { OAuth2Service } from "./nodefony/service/oauth2.js";
49
+ import { TOKEN_FACETS } from "./nodefony/src/token/tokenFilters.js";
50
+ import { ApiKeyError } from "./nodefony/errors/ApiKeyError.js";
51
+ import { ApiKeyService } from "./nodefony/service/apiKeys.js";
52
+ import { MemoryAuditStore } from "./nodefony/src/audit/MemoryAuditStore.js";
53
+ import { getAuditStoreFactory, listAuditStores, registerAuditStore } from "./nodefony/src/audit/auditStoreRegistry.js";
54
+ import AuditService from "./nodefony/service/auditService.js";
55
+ import { MemoryTotpSecretStore } from "./nodefony/src/totp/MemoryTotpSecretStore.js";
56
+ import { getTotpStoreFactory, listTotpStores, registerTotpStore } from "./nodefony/src/totp/totpSecretStoreRegistry.js";
57
+ import { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey } from "./nodefony/src/crypto/secretCipher.js";
58
+ import { deriveTotpKey } from "./nodefony/src/totp/totpCipher.js";
59
+ import { base32Decode, totpCode } from "./nodefony/src/totp/totpCrypto.js";
60
+ import { beginTotpEnrollment, confirmTotpEnrollment, disableTotp, totpStatus, verifyTotpLogin } from "./nodefony/src/totp/totpOperations.js";
61
+ import { TotpService } from "./nodefony/service/totp.js";
62
+ import { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS } from "./nodefony/src/webhook/webhookSort.js";
63
+ import { MemoryWebhookStore } from "./nodefony/src/webhook/MemoryWebhookStore.js";
64
+ import { getWebhookStoreFactory, listWebhookStores, registerWebhookStore } from "./nodefony/src/webhook/webhookStoreRegistry.js";
65
+ import { SsrfError } from "./nodefony/errors/SsrfError.js";
66
+ import { assertPublicUrl, isBlockedAddress } from "./nodefony/src/net/ssrfGuard.js";
67
+ import { WebhookService } from "./nodefony/service/webhooks.js";
68
+ import SecuritySecrets from "./nodefony/command/security-secrets.js";
69
+ import SecurityUserAdd from "./nodefony/command/security-user-add.js";
70
+ import SecurityUserList from "./nodefony/command/security-user-list.js";
71
+ import SecurityUserDelete from "./nodefony/command/security-user-delete.js";
72
+ import SecurityToken from "./nodefony/command/security-token.js";
73
+ import { createSecurityAdminApi, parseAuditQuery, registerSecurityAdminApi } from "./nodefony/src/admin/SecurityAdminApi.js";
74
+ import { registerUserRevocationCascade } from "./nodefony/src/admin/userRevocationCascade.js";
75
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
76
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
77
+ import { tokenStatusCriteria } from "./nodefony/src/token/tokenCriteria.js";
78
+ import { AccessDeniedError } from "./nodefony/errors/AccessDeniedError.js";
79
+ import "./nodefony/errors/index.js";
80
+ import "./nodefony/contracts/index.js";
81
+ import { Kernel, Module, services } from "nodefony";
82
+ import { fileURLToPath } from "node:url";
83
+ import { registerUserAdminApi } from "@nodefony/user";
84
+ //#region index.ts
85
+ /**
86
+ * `@nodefony/security` — couche de sécurité de Nodefony (refonte 2026, P6).
87
+ *
88
+ * Identité **hybride** : session serveur cookie opaque (BFF) par défaut web/Studio,
89
+ * JWT réservé API/M2M/agents. Pattern **`IAuthenticator`** + registre de fabriques
90
+ * (pluggable), **Zero Trust** par défaut, config type-safe `defineSecurityConfig()`
91
+ * + Zod validée au boot (fail-closed si invalide). Consomme `@nodefony/user`
92
+ * (IUser/IUserProvider/IPasswordVerifier) — jamais l'inverse.
93
+ *
94
+ * Livré : firewall (zones, mode first|all, challenge RFC 7235), authenticators
95
+ * `anonymous`/`userpassword` (Basic RFC 7617)/`session` (BFF, J3), flux
96
+ * login/logout/me (`AuthFlow`, anti-fixation + throttling partagé).
97
+ * Jwt/oauth2/mtls/apikey, CORS, CSRF, autorisation par décorateurs et data
98
+ * plane Studio arrivent aux sessions suivantes (plan J0→J10).
99
+ */
100
+ let Security = class Security extends Module {
101
+ constructor(kernel) {
102
+ super("security", kernel, fileURLToPath(import.meta.url), config);
103
+ this.addCommand(SecuritySecrets);
104
+ this.addCommand(SecurityUserAdd);
105
+ this.addCommand(SecurityUserList);
106
+ this.addCommand(SecurityUserDelete);
107
+ this.addCommand(SecurityToken);
108
+ }
109
+ /**
110
+ * JSON Schema de la config security → data plane admin (config riche Studio).
111
+ * ⚠️ Les VALEURS effectives des secrets (csrf/jwt/oauth) sont redactées côté
112
+ * serveur (`safeConfig`) AVANT envoi — le schéma ne décrit que la structure.
113
+ */
114
+ configSchema() {
115
+ return securityConfigJsonSchema();
116
+ }
117
+ /**
118
+ * Enregistre les data planes admin **sécurité** (`/nodefony/security/api/*`,
119
+ * P6.14) ET **utilisateur** (`/nodefony/user/api/*`, P6.15) auprès du broker
120
+ * AVANT que framework ne monte les routes (`onReady`). Le data plane user est
121
+ * DÉFINI dans `@nodefony/user` (propriétaire du domaine) mais monté ici, car
122
+ * `@nodefony/user` est une lib pure non-bootable et `security` en dépend déjà.
123
+ * No-op si le broker est absent (Studio non chargé) ou déjà enregistré.
124
+ */
125
+ async onKernelBoot() {
126
+ const container = this.kernel?.container;
127
+ const registry = container?.get("adminBroker");
128
+ if (registry && container) {
129
+ registerSecurityAdminApi(registry, container);
130
+ registerUserAdminApi(registry, container);
131
+ }
132
+ if (this.kernel && container) registerUserRevocationCascade(this.kernel, container);
133
+ return this;
134
+ }
135
+ };
136
+ Security = __decorate([services([
137
+ Firewall,
138
+ AuthFlow,
139
+ TokenService,
140
+ AccessTokenVerifierService,
141
+ ApiKeyService,
142
+ Authorization,
143
+ WebAuthnService,
144
+ OAuth2Service,
145
+ AuditService,
146
+ TotpService,
147
+ WebhookService
148
+ ]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Security);
149
+ var security_default = Security;
150
+ //#endregion
151
+ export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InvalidTargetError, JwtAuthenticator, JwtKeystore, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Service, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, disableTotp, encryptSecret, generateApiKey, generateEphemeralKey, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, resolveJwtRuntime, securityConfigJsonSchema, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
@@ -0,0 +1,158 @@
1
+ import { readIfPresentSync } from "../src/token/secretFile.js";
2
+ import { Command } from "nodefony";
3
+ import { randomBytes } from "node:crypto";
4
+ import path from "node:path";
5
+ import { appendFileSync } from "node:fs";
6
+ import { spawnSync } from "node:child_process";
7
+ //#region nodefony/command/security-secrets.ts
8
+ const options = {
9
+ helpGroup: "COMPTES ET SECRETS",
10
+ showBanner: false,
11
+ kernelEvent: "onReady"
12
+ };
13
+ const CYAN = "\x1B[36m";
14
+ const GREEN = "\x1B[32m";
15
+ const YELLOW = "\x1B[33m";
16
+ const DIM = "\x1B[2m";
17
+ const BOLD = "\x1B[1m";
18
+ const RESET = "\x1B[0m";
19
+ /** Les 3 clés générées (nom d'env var → rôle affiché). */
20
+ const KEYS = [
21
+ "NF_TOTP_KEY",
22
+ "NF_WEBHOOK_KEY",
23
+ "NF_CSRF_SECRET"
24
+ ];
25
+ /**
26
+ * Ce que chaque secret PROTÈGE, et ce qui casse sans lui.
27
+ *
28
+ * 🔴 Sans ce catalogue, la commande était muette sur l'essentiel : quand les
29
+ * trois clés étaient en place, elle affichait trois « ✓ » et RIEN d'autre — ni
30
+ * les noms, ni les rôles. On ne savait donc ni ce qui avait été généré, ni
31
+ * pourquoi. Un secret qu'on ne comprend pas est un secret qu'on ne fait jamais
32
+ * tourner, et qu'on recopie d'un environnement à l'autre.
33
+ *
34
+ * La conséquence est écrite au présent et pour la PRODUCTION : c'est là qu'une
35
+ * clé absente cesse d'être un avertissement de développement.
36
+ */
37
+ const ROLES = {
38
+ NF_TOTP_KEY: {
39
+ protected: "chiffre le secret 2FA de chaque compte au repos (AES-256-GCM)",
40
+ without: "2FA désactivé en production — un secret chiffré par une clé éphémère serait illisible au redémarrage"
41
+ },
42
+ NF_WEBHOOK_KEY: {
43
+ protected: "chiffre les secrets de signature des webhooks au repos",
44
+ without: "webhooks désactivés en production (fail-safe, jamais de signature muette)"
45
+ },
46
+ NF_CSRF_SECRET: {
47
+ protected: "signe les jetons anti-rejeu des mutations (`@CsrfProtect`)",
48
+ without: "en cluster, le jeton émis par un pod est rejeté par les autres"
49
+ },
50
+ "jwt.keystore": {
51
+ protected: "signe les JWT (clé Ed25519, rotation gérée par le keystore)",
52
+ without: "chaque process signe avec la sienne : un jeton émis par la CLI est refusé par le serveur"
53
+ }
54
+ };
55
+ /**
56
+ * `nodefony security:secrets` — génère les clés de chiffrement attendues par le
57
+ * module security (TOTP, webhooks, CSRF) au bon format (32 octets aléatoires,
58
+ * base64) et guide le câblage en 3 FICHIERS (.env → env.ts → nodefony.config.ts).
59
+ * Réponse directe aux warnings « clé ÉPHÉMÈRE générée » du boot.
60
+ *
61
+ * DX anti-confusion (vécu : blocs collés dans le shell → parse error zsh) :
62
+ * - chaque étape nomme son FICHIER et rappelle que rien ne se tape au terminal ;
63
+ * - les étapes déjà faites sont DÉTECTÉES (grep des fichiers du projet) et
64
+ * affichées `✓` au lieu de redemander un collage ;
65
+ * - `--write` écrit le `.env` (fichier local gitignoré) : ajoute uniquement les
66
+ * clés ABSENTES, ne remplace jamais une valeur existante (rotation = manuelle).
67
+ * `env.ts` et `nodefony.config.ts` ne sont JAMAIS modifiés (code de l'app).
68
+ */
69
+ var SecuritySecrets = class extends Command {
70
+ constructor(cli) {
71
+ super("security:secrets", "engendre les clés de chiffrement du module security", cli, options);
72
+ this.addOption("-j, --json", "sortie JSON (scripts/CI)");
73
+ this.addOption("-w, --write", "écrit les clés manquantes dans le .env du projet (jamais de remplacement)");
74
+ }
75
+ /** Racine du projet (kernel booté) — repli cwd. */
76
+ #root() {
77
+ return this.kernel?.path ?? process.cwd();
78
+ }
79
+ /**
80
+ * `true` si `.env.local` est SUIVI par git — y écrire des secrets les mènerait
81
+ * au commit (convention B : `*.local` doit être gitignoré). Best-effort : git
82
+ * absent / hors repo → `false` (on écrit).
83
+ */
84
+ #dotenvTracked() {
85
+ try {
86
+ return spawnSync("git", [
87
+ "ls-files",
88
+ "--error-unmatch",
89
+ ".env.local"
90
+ ], {
91
+ cwd: this.#root(),
92
+ stdio: "ignore"
93
+ }).status === 0;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+ /** Contenu d'un fichier du projet, "" si absent/illisible (détection best-effort). */
99
+ #read(file) {
100
+ try {
101
+ return readIfPresentSync(path.resolve(this.#root(), file)) ?? "";
102
+ } catch {
103
+ return "";
104
+ }
105
+ }
106
+ async generate(opts) {
107
+ const gen = () => randomBytes(32).toString("base64");
108
+ const secrets = {};
109
+ for (const k of KEYS) secrets[k] = gen();
110
+ if (opts.json) {
111
+ process.stdout.write(JSON.stringify(secrets, null, 2) + "\n");
112
+ return this;
113
+ }
114
+ const dotenvLocal = this.#read(".env.local");
115
+ const dotenv = this.#read(".env") + "\n" + dotenvLocal;
116
+ const envTs = this.#read("env.ts");
117
+ const cfgTs = this.#read("nodefony.config.ts");
118
+ const missingInDotenv = KEYS.filter((k) => !new RegExp(`^\\s*${k}\\s*=`, "m").test(dotenv));
119
+ const missingInEnvTs = KEYS.filter((k) => !envTs.includes(k));
120
+ const WIRING = {
121
+ NF_TOTP_KEY: ` totp: { encryptionKey: ctx.env.NF_TOTP_KEY },`,
122
+ NF_WEBHOOK_KEY: ` webhooks: { encryptionKey: ctx.env.NF_WEBHOOK_KEY },`,
123
+ NF_CSRF_SECRET: ` csrf: { secret: ctx.env.NF_CSRF_SECRET },`
124
+ };
125
+ const missingInCfg = KEYS.filter((k) => !cfgTs.includes(k));
126
+ const w = (s) => {
127
+ process.stdout.write(s);
128
+ };
129
+ w(`\n${BOLD}🔐 Secrets du module security${RESET} ${DIM}— 4 secrets, 3 fichiers${RESET}\n\n`);
130
+ for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "jwt.keystore" ? /keystore\s*:/u.test(cfgTs) : new RegExp(`^\\s*${name}\\s*=`, "m").test(dotenv)) ? "\x1B[32m✓" : "\x1B[33m○"}${RESET} ${BOLD}${name.padEnd(16)}${RESET}${DIM}${role.protected}${RESET}\n ${DIM}sans → ${role.without}${RESET}\n`);
131
+ const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(dotenvLocal);
132
+ w(`\n${DIM} · NF_MCP_TOKEN n'est PAS un secret de cette application, et n'a rien à\n faire dans cette liste : c'est un jeton qu'elle ÉMET, présenté par un\n agent pour entrer. Aucun code d'ici ne le lit. Il se pose chez l'agent\n qui le porte — nodefony security:token --write.${RESET}\n`);
133
+ if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env.local — rien ne la lit,\n tu peux la retirer.${RESET}\n`);
134
+ w(`\n${YELLOW}⚠ rien ne se tape dans le terminal : chaque bloc se colle dans le fichier indiqué.${RESET}\n\n`);
135
+ w(`${BOLD}1. Fichier ${CYAN}.env.local${RESET}${BOLD} — les valeurs${RESET} ${DIM}(gitignoré ; .env commité = défauts NON-secrets)${RESET}\n`);
136
+ if (missingInDotenv.length === 0) w(` ${GREEN}✓ les 3 clés y sont déjà${RESET} ${DIM}(rien à faire — rotation = remplacer la valeur à la main)${RESET}\n\n`);
137
+ else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env.local est suivi par git — je n'y écris PAS de secrets.${RESET}\n ${DIM}Ajoute \`*.local\` au .gitignore (et \`git rm --cached .env.local\`), puis relance --write ;\n ou colle les lignes ci-dessous à la main :${RESET}\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n\n`);
138
+ else if (opts.write) {
139
+ const block = (dotenvLocal && !dotenvLocal.endsWith("\n") ? "\n" : "") + `# clés security — générées par \`nodefony security:secrets\`\n` + missingInDotenv.map((k) => `${k}=${secrets[k]}`).join("\n") + "\n";
140
+ appendFileSync(path.resolve(this.#root(), ".env.local"), block);
141
+ w(` ${GREEN}✓ écrit dans .env.local${RESET} ${DIM}(${missingInDotenv.join(", ")} — les clés déjà présentes n'ont pas été touchées)${RESET}\n\n`);
142
+ } else w(` colle ces lignes ${DIM}(ou relance avec ${RESET}${CYAN}--write${RESET}${DIM} pour que je les écrive)${RESET} :\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n ${DIM}(en prod : Secret k8s / vault — jamais en git)${RESET}\n\n`);
143
+ w(`${BOLD}2. Fichier ${CYAN}env.ts${RESET}${BOLD} — la déclaration typée${RESET} ${DIM}(env.ts est le seul lecteur de process.env)${RESET}\n`);
144
+ if (missingInEnvTs.length === 0) w(` ${GREEN}✓ déjà déclarées${RESET}\n\n`);
145
+ else w(` ajoute dans le defineEnv({ … }) :\n\n` + missingInEnvTs.map((k) => ` ${k}: envString({ optional: true }),`).join("\n") + `\n\n`);
146
+ w(`${BOLD}3. Fichier ${CYAN}nodefony.config.ts${RESET}${BOLD} — le câblage vers le module security${RESET}\n`);
147
+ if (missingInCfg.length === 0) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
148
+ else w(" complète l'entrée security du manifeste modules :\n\n use(\"@nodefony/security\", {\n" + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n }),\n\n`);
149
+ const jwtCable = /keystore\s*:/u.test(cfgTs);
150
+ w(`${BOLD}4. Fichier ${CYAN}nodefony.config.ts${RESET}${BOLD} — les clés de SIGNATURE des jetons${RESET} ${DIM}(jwt.keystore)${RESET}\n`);
151
+ if (jwtCable) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
152
+ else w(` ${YELLOW}⚠ absentes : chaque process signe avec une clé ÉPHÉMÈRE.${RESET}\n ${DIM}Un jeton émis par la CLI porte alors un \`kid\` que le serveur ne\n connaît pas, et il est refusé — et un redémarrage invalide les jetons\n en vol. Ce n'est pas une valeur à coller : c'est une SOURCE à déclarer.${RESET}\n\n use("@nodefony/security", {\n jwt: { keystore: ctx.isProd ? {} : { dir: "var/keys" } },\n }),\n\n ${DIM}En production, le dossier n'a pas de sens (pods jetables) : la clé vient\n de l'environnement — jwt.keystore.keySetJson, injecté par ton gestionnaire\n de secrets et partagé par tous les pods.${RESET}\n\n`);
153
+ w(`${DIM}Pourquoi 3 fichiers ? .env.local porte la VALEUR (secret machine, gitignoré —\nle .env commité ne porte que des défauts non-secrets) ; env.ts la DÉCLARE\n(catalogue typé, validé au boot) ; nodefony.config.ts la CÂBLE au module.\nLes étapes 2 et 3 ne se font qu'une fois — ensuite seule l'étape 1 vit.\nL'étape 4 sort de ce schéma, et c'est voulu : un keyset n'est pas une valeur\nqu'on colle, mais une source que le keystore gère (rotation, permissions).${RESET}\n\nRelance le serveur : plus aucun warning « clé ÉPHÉMÈRE » au boot.\n\n`);
154
+ return this;
155
+ }
156
+ };
157
+ //#endregion
158
+ export { SecuritySecrets as default };