@nodefony/security 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +182 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +151 -0
  6. package/dist/nodefony/command/security-secrets.js +158 -0
  7. package/dist/nodefony/command/security-token.js +335 -0
  8. package/dist/nodefony/command/security-user-add.js +131 -0
  9. package/dist/nodefony/command/security-user-delete.js +102 -0
  10. package/dist/nodefony/command/security-user-list.js +77 -0
  11. package/dist/nodefony/config/config.js +366 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +35 -0
  13. package/dist/nodefony/contracts/IAccessVoter.js +13 -0
  14. package/dist/nodefony/contracts/IApiKey.js +1 -0
  15. package/dist/nodefony/contracts/IAuditEvent.js +1 -0
  16. package/dist/nodefony/contracts/IAuditStore.js +1 -0
  17. package/dist/nodefony/contracts/IAuthenticator.js +1 -0
  18. package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
  19. package/dist/nodefony/contracts/IFirewall.js +1 -0
  20. package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
  21. package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
  22. package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
  23. package/dist/nodefony/contracts/ISecuredArea.js +1 -0
  24. package/dist/nodefony/contracts/IToken.js +1 -0
  25. package/dist/nodefony/contracts/ITokenStore.js +1 -0
  26. package/dist/nodefony/contracts/ITotpSecret.js +1 -0
  27. package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
  28. package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
  29. package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
  30. package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
  31. package/dist/nodefony/contracts/IWebhookStore.js +1 -0
  32. package/dist/nodefony/contracts/index.js +2 -0
  33. package/dist/nodefony/errors/AccessDeniedError.js +14 -0
  34. package/dist/nodefony/errors/ApiKeyError.js +21 -0
  35. package/dist/nodefony/errors/AuthenticationError.js +14 -0
  36. package/dist/nodefony/errors/CsrfError.js +23 -0
  37. package/dist/nodefony/errors/InvalidTargetError.js +39 -0
  38. package/dist/nodefony/errors/SsrfError.js +17 -0
  39. package/dist/nodefony/errors/ThrottledError.js +21 -0
  40. package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
  41. package/dist/nodefony/errors/WebAuthnError.js +21 -0
  42. package/dist/nodefony/errors/index.js +9 -0
  43. package/dist/nodefony/service/accessTokenVerifier.js +77 -0
  44. package/dist/nodefony/service/apiKeys.js +310 -0
  45. package/dist/nodefony/service/auditService.js +145 -0
  46. package/dist/nodefony/service/authFlow.js +332 -0
  47. package/dist/nodefony/service/authorization.js +95 -0
  48. package/dist/nodefony/service/cors.js +81 -0
  49. package/dist/nodefony/service/csrf.js +97 -0
  50. package/dist/nodefony/service/firewall.js +699 -0
  51. package/dist/nodefony/service/oauth2.js +153 -0
  52. package/dist/nodefony/service/securityHeaders.js +80 -0
  53. package/dist/nodefony/service/tokenService.js +486 -0
  54. package/dist/nodefony/service/totp.js +209 -0
  55. package/dist/nodefony/service/webAuthn.js +343 -0
  56. package/dist/nodefony/service/webhooks.js +539 -0
  57. package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
  58. package/dist/nodefony/src/SecuredArea.js +51 -0
  59. package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
  60. package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
  61. package/dist/nodefony/src/admin/adminAudit.js +37 -0
  62. package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
  63. package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
  64. package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
  65. package/dist/nodefony/src/audit/auditBridge.js +82 -0
  66. package/dist/nodefony/src/audit/auditFilters.js +60 -0
  67. package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
  68. package/dist/nodefony/src/audit/readAuditContext.js +24 -0
  69. package/dist/nodefony/src/audit/recordAudit.js +16 -0
  70. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
  71. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
  72. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
  73. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
  74. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
  75. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
  76. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
  77. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
  78. package/dist/nodefony/src/authenticator/bearer.js +2 -0
  79. package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
  80. package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
  81. package/dist/nodefony/src/crypto/secretCipher.js +79 -0
  82. package/dist/nodefony/src/csp.js +54 -0
  83. package/dist/nodefony/src/csrfToken.js +65 -0
  84. package/dist/nodefony/src/net/ssrfGuard.js +130 -0
  85. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +65 -0
  87. package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
  88. package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
  89. package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
  90. package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
  91. package/dist/nodefony/src/sessionIdentity.js +35 -0
  92. package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
  93. package/dist/nodefony/src/token/AnonymousToken.js +40 -0
  94. package/dist/nodefony/src/token/JwtKeystore.js +160 -0
  95. package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
  96. package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
  97. package/dist/nodefony/src/token/UserToken.js +67 -0
  98. package/dist/nodefony/src/token/jwtRuntime.js +19 -0
  99. package/dist/nodefony/src/token/secretFile.js +134 -0
  100. package/dist/nodefony/src/token/tokenCriteria.js +35 -0
  101. package/dist/nodefony/src/token/tokenFilters.js +72 -0
  102. package/dist/nodefony/src/token/tokenSort.js +40 -0
  103. package/dist/nodefony/src/token/tokenStatus.js +35 -0
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
  106. package/dist/nodefony/src/totp/totpCipher.js +30 -0
  107. package/dist/nodefony/src/totp/totpCrypto.js +226 -0
  108. package/dist/nodefony/src/totp/totpOperations.js +129 -0
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
  110. package/dist/nodefony/src/voter/RoleVoter.js +32 -0
  111. package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
  112. package/dist/nodefony/src/voter/voterRegistry.js +20 -0
  113. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
  114. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
  115. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
  116. package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
  117. package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
  118. package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
  119. package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
  120. package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
  121. package/dist/nodefony/src/webhook/webhookSort.js +48 -0
  122. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
  123. package/dist/types/index.d.ts +157 -0
  124. package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
  125. package/dist/types/nodefony/command/security-token.d.ts +44 -0
  126. package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
  127. package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
  128. package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
  129. package/dist/types/nodefony/config/config.d.ts +295 -0
  130. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  131. package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
  132. package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
  133. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
  134. package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
  135. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
  136. package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
  137. package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
  138. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
  139. package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
  140. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
  141. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
  142. package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
  143. package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
  144. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
  145. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
  146. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
  147. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
  148. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
  149. package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
  150. package/dist/types/nodefony/contracts/index.d.ts +9 -0
  151. package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
  152. package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
  153. package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
  154. package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
  155. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
  156. package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
  157. package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
  158. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
  159. package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
  160. package/dist/types/nodefony/errors/index.d.ts +8 -0
  161. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
  162. package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
  163. package/dist/types/nodefony/service/auditService.d.ts +30 -0
  164. package/dist/types/nodefony/service/authFlow.d.ts +123 -0
  165. package/dist/types/nodefony/service/authorization.d.ts +33 -0
  166. package/dist/types/nodefony/service/cors.d.ts +48 -0
  167. package/dist/types/nodefony/service/csrf.d.ts +57 -0
  168. package/dist/types/nodefony/service/firewall.d.ts +148 -0
  169. package/dist/types/nodefony/service/oauth2.d.ts +66 -0
  170. package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
  171. package/dist/types/nodefony/service/tokenService.d.ts +103 -0
  172. package/dist/types/nodefony/service/totp.d.ts +58 -0
  173. package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
  174. package/dist/types/nodefony/service/webhooks.d.ts +160 -0
  175. package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
  176. package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
  177. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
  178. package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
  179. package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
  180. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
  181. package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
  182. package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
  183. package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
  184. package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
  185. package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
  186. package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
  187. package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
  188. package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
  189. package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
  190. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
  191. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
  192. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
  193. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
  194. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
  195. package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
  196. package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
  197. package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
  198. package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
  199. package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
  200. package/dist/types/nodefony/src/csp.d.ts +39 -0
  201. package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
  202. package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
  203. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
  204. package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
  205. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
  206. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
  207. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
  208. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
  209. package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
  210. package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
  211. package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
  212. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
  213. package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
  214. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
  215. package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
  216. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
  217. package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
  218. package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
  219. package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
  220. package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
  221. package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
  222. package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
  223. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
  224. package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
  225. package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
  226. package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
  227. package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
  228. package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
  229. package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
  230. package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
  231. package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
  232. package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
  233. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
  234. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
  235. package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
  236. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
  237. package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
  238. package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
  239. package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
  240. package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
  241. package/docs/api-keys.md +691 -0
  242. package/docs/audit.md +751 -0
  243. package/docs/authenticators.md +487 -0
  244. package/docs/authorization.md +497 -0
  245. package/docs/cors.md +497 -0
  246. package/docs/csrf.md +392 -0
  247. package/docs/external-jwt.md +181 -0
  248. package/docs/firewall.md +546 -0
  249. package/docs/headers.md +616 -0
  250. package/docs/index.md +207 -0
  251. package/docs/lexique.md +190 -0
  252. package/docs/oauth2.md +575 -0
  253. package/docs/obtenir-un-jeton.md +225 -0
  254. package/docs/tokens.md +520 -0
  255. package/docs/totp.md +804 -0
  256. package/docs/webauthn.md +733 -0
  257. package/docs/webhooks.md +1016 -0
  258. package/package.json +83 -0
@@ -0,0 +1,616 @@
1
+ ---
2
+ title: "En-têtes de sécurité — le contrat passé au navigateur"
3
+ navTitle: En-têtes de sécurité
4
+ lang: fr
5
+ module: "@nodefony/security"
6
+ topic: headers
7
+ coverageModule: security
8
+ coverageFiles: "securityHeaders.ts,csp.ts"
9
+ section: "Sécurité"
10
+ audience: [developer, devops]
11
+ tags:
12
+ [
13
+ security,
14
+ headers,
15
+ csp,
16
+ nonce,
17
+ hsts,
18
+ clickjacking,
19
+ nosniff,
20
+ referrer-policy,
21
+ coop,
22
+ coep,
23
+ corp,
24
+ permissions-policy,
25
+ owasp,
26
+ ]
27
+ version: "doc"
28
+ status: stable
29
+ updated: 2026-07-19
30
+ source: "src/packages/@nodefony/security/docs/headers.md"
31
+ ---
32
+
33
+ # En-têtes de sécurité — le contrat passé au navigateur
34
+
35
+ > Ton serveur ne contrôle pas le navigateur de tes visiteurs — il ne peut que **lui donner des
36
+ > ordres**, et ces ordres sont des en-têtes HTTP. Une douzaine de lignes ferment des classes
37
+ > entières d'attaques : XSS, clickjacking, sniffing MIME, fuite d'URL, downgrade HTTPS. Nodefony
38
+ > les pose en **deux couches, une seule autorité par en-tête** : le socle **transport**
39
+ > (`@nodefony/http`, dès l'entrée brute — couvre aussi les fichiers statiques et les erreurs) et la
40
+ > couche **applicative** (`@nodefony/security`, dans le pipeline — CSP, Referrer-Policy, isolation
41
+ > cross-origin). Ancré sur `SecurityHeaders` (`securityHeaders.ts:42`) et
42
+ > `Firewall.applySecurityHeaders()` (`firewall.ts:1029`).
43
+
44
+ 📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **En-têtes de sécurité**
45
+
46
+ ## 🧠 Le modèle mental — deux couches, deux moments
47
+
48
+ Un en-tête de sécurité n'a de valeur que s'il est **sur toutes les réponses**. Le piège classique
49
+ n'est pas d'en oublier un : c'est de le poser **de façon inégale** — présent sur les routes de
50
+ contrôleur, absent sur un fichier statique ou une page d'erreur 404, c'est-à-dire exactement là où
51
+ un attaquant dépose son contenu.
52
+
53
+ D'où le découpage : ce qui doit couvrir **tout ce qui sort du process** est posé au plus tôt ; ce
54
+ qui dépend de la **réponse applicative** (le CSP, qui doit connaître le nonce et la route) est posé
55
+ après le routage.
56
+
57
+ ```mermaid
58
+ flowchart TD
59
+ RAW["Requête entrante"] --> T["onHttpRequest — socle TRANSPORT (@nodefony/http)<br/>X-Content-Type-Options · X-Frame-Options · HSTS (TLS seulement)"]
60
+ T --> COV["couvre AUSSI : fichiers statiques, 404/500,<br/>et un serveur SANS module security"]
61
+ T --> PIPE["pipeline : routing / resolve"]
62
+ PIPE --> A["applySecurityHeaders — couche APPLICATIVE (@nodefony/security)<br/>CSP · Referrer-Policy · COOP/COEP/CORP · Origin-Agent-Cluster · Permissions-Policy"]
63
+ A --> CSPQ{"le CSP porte-t-il<br/>un nonce ?"}
64
+ CSPQ -->|non| STAT["CSP figé au boot — 0 allocation par requête"]
65
+ CSPQ -->|oui| NONCE["cspFor(nonce) — 1 join par requête<br/>nonce généré paresseusement sur le Context"]
66
+ ```
67
+
68
+ **Une seule source par en-tête** : `@nodefony/security` ne ré-émet **jamais** les trois en-têtes
69
+ transport — c'est écrit noir sur blanc dans le contrat de la couche applicative
70
+ (`ISecurityHeadersOptions`, `securityHeaders.ts:12`). Pas de double émission, donc pas de valeurs
71
+ contradictoires sur la même réponse.
72
+
73
+ ## 📖 Lexique
74
+
75
+ | Terme | Développé — et ce que ça veut dire |
76
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
77
+ | CSP | _Content-Security-Policy_ : liste blanche des sources autorisées (scripts, styles, images…). Première barrière anti-XSS. |
78
+ | XSS | _Cross-Site Scripting_ : un attaquant fait exécuter **son** JavaScript dans la page de ta victime, avec ses cookies. |
79
+ | Nonce | _number used once_ : jeton aléatoire régénéré à **chaque requête**, qui autorise un `<script>` inline **précis** et lui seul. |
80
+ | Clickjacking | Ton site est chargé en `<iframe>` transparente au-dessus d'un piège : la victime croit cliquer ailleurs, elle clique chez toi. |
81
+ | MIME sniffing | Le navigateur ignore le `Content-Type` et **devine** le type d'un fichier — un `.txt` uploadé peut finir exécuté comme script. |
82
+ | HSTS | _HTTP Strict-Transport-Security_ (RFC 6797) : le navigateur mémorise « ce domaine, c'est HTTPS uniquement ». |
83
+ | Referrer | En-tête que le navigateur envoie au site suivant pour dire d'où l'on vient — donc une **fuite d'URL** potentielle. |
84
+ | COOP / COEP / CORP | _Cross-Origin **Opener** / **Embedder** / **Resource** Policy_ : trois verrous d'isolation entre origines (Spectre, vol d'assets). |
85
+ | OAC | _Origin-Agent-Cluster_ : demande au navigateur d'isoler l'origine dans son propre processus/heap. |
86
+ | Permissions-Policy | Coupe l'accès aux API sensibles du navigateur (caméra, micro, géolocalisation) pour la page **et ses iframes**. |
87
+ | Fragment CSP | Directives additionnelles déclarées par un module (`directive → sources`), fusionnées dans le CSP de base. |
88
+ | Downgrade | Un attaquant réseau force la connexion en HTTP clair pour la lire ou la modifier. |
89
+
90
+ ## Qu'est-ce que c'est ? — un panneau d'instructions collé sur chaque réponse
91
+
92
+ Imagine que tu envoies un colis. Le contenu, c'est ton HTML. Les en-têtes de sécurité, c'est
93
+ l'**étiquette** collée dessus : « ne pas ouvrir avec un autre outil que celui-ci », « ne pas
94
+ transporter dans un autre camion », « interdiction de recopier l'adresse de l'expéditeur ». Le
95
+ transporteur — le navigateur — les respecte. Sans étiquette, il improvise, et improviser c'est
96
+ exactement ce qu'un attaquant attend.
97
+
98
+ Chaque en-tête ferme **une** faille concrète :
99
+
100
+ - **CSP** — un attaquant réussit à injecter `<script src="https://evil.tld/x.js">` dans un
101
+ commentaire de ton site ; sans CSP, le navigateur l'exécute avec la session de la victime.
102
+ - **X-Frame-Options / `frame-ancestors`** — un site pirate charge ta page « Supprimer mon compte »
103
+ en iframe invisible sous un bouton « Jouer » : la victime clique, c'est chez toi que ça s'applique.
104
+ - **`nosniff`** — un avatar téléversé est en réalité du JavaScript ; un navigateur « serviable »
105
+ devine le type et l'exécute **sur ton origine**, donc avec tes cookies.
106
+ - **Referrer-Policy** — un clic vers l'extérieur transmet ton URL interne complète
107
+ (`/admin/facture/8123?client=ACME`) dans le `Referer` du site suivant.
108
+ - **HSTS** — sur un Wi-Fi public, la première requête part en clair et peut être interceptée puis
109
+ maintenue en HTTP ; HSTS mémorisé force le HTTPS **avant** toute émission.
110
+ - **COOP / COEP / CORP** — isolent ton document des autres origines (fenêtres ouvrantes, ressources
111
+ embarquées) : réponse aux canaux auxiliaires type Spectre et au vol d'assets par inclusion.
112
+
113
+ Le détail de chaque en-tête — menace, valeur par défaut, compromis — est dans le catalogue plus bas.
114
+
115
+ ## La vision Nodefony — pré-calculé au boot, quasi gratuit par requête
116
+
117
+ Un framework qui recalcule ses en-têtes à chaque requête paie ce confort en allocations. Nodefony
118
+ fait l'inverse : **tout ce qui est constant est calculé une fois au démarrage**.
119
+
120
+ - `SecurityHeaders` (`securityHeaders.ts:42`) construit **au boot** la table des en-têtes constants
121
+ (Referrer-Policy, COOP/COEP/CORP, Origin-Agent-Cluster, Permissions-Policy, et le CSP quand il est
122
+ statique) et la **gèle** avec `Object.freeze` (`securityHeaders.ts:77`). Par requête, le firewall
123
+ se contente de la parcourir et de la poser : zéro concaténation, zéro objet créé.
124
+ - Côté transport, même principe : `HttpKernel.computeSecurityHeaderCaches()`
125
+ (`http-kernel.ts:330`) précalcule la chaîne HSTS (`max-age`, `includeSubDomains`, `preload`) au
126
+ boot ; `onHttpRequest` (`http-kernel.ts:819`) ne fait plus que trois `setHeader`.
127
+ - Le seul coût variable est le **nonce CSP**, et il est **paresseux** : `Context.cspNonce`
128
+ (`Context.ts:253`) ne génère ses 128 bits (`randomBytes(16)` en base64) qu'à la première lecture,
129
+ puis mémoïse. Une réponse qui n'a aucun script inline à signer ne paie aucun appel crypto.
130
+
131
+ Le second parti pris est la **séparation d'autorité** décrite plus haut : un seul émetteur par
132
+ en-tête, donc un comportement prévisible et testable — le banc live vérifie les deux couches sur la
133
+ même réponse (`security-headers.test.ts:30`).
134
+
135
+ > [!IMPORTANT]
136
+ > `@nodefony/security` est **optionnel**, pas le socle. Une app Nodefony sans module security émet
137
+ > quand même `nosniff`, `X-Frame-Options` et HSTS : c'est du _secure-by-default_. Ce que tu perds
138
+ > sans security, c'est le CSP, la Referrer-Policy et l'isolation cross-origin.
139
+
140
+ ## 🚀 Démarrage rapide
141
+
142
+ Point de départ : une app générée par `nodefony create app`. Les en-têtes sont **déjà actifs** — ce
143
+ que tu écris ci-dessous, ce sont tes **écarts** au défaut.
144
+
145
+ ### 1. Déclarer la politique dans `nodefony.config.ts`
146
+
147
+ ```typescript
148
+ // nodefony.config.ts — l'app n'écrit QUE ses écarts ; le reste prend le défaut du framework.
149
+ import { defineConfig, use } from "nodefony";
150
+
151
+ export default defineConfig(() => ({
152
+ modules: [
153
+ "@nodefony/http",
154
+ "@nodefony/framework",
155
+ use("@nodefony/security", {
156
+ headers: {
157
+ // `{{nonce}}` est substitué par un jeton FRAIS à chaque requête (cf plus bas).
158
+ csp:
159
+ "default-src 'self'; script-src 'self' 'nonce-{{nonce}}'; " +
160
+ "style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
161
+ "object-src 'none'; base-uri 'self'; form-action 'self'",
162
+ cspNonces: true,
163
+ // Ne fuiter que l'origine, et rien vers un site en clair.
164
+ referrerPolicy: "strict-origin-when-cross-origin",
165
+ // Isolation cross-origin : ABSENTE par défaut, on l'active explicitement.
166
+ coop: "same-origin",
167
+ corp: "same-origin",
168
+ permissionsPolicy: "camera=(), microphone=(), geolocation=()",
169
+ },
170
+ }),
171
+ ],
172
+ }));
173
+ ```
174
+
175
+ Les clés sont **typées et auto-complétées** : le module augmente le registre `NodefonyModuleConfig`
176
+ du core (`index.ts:28`), donc `use("@nodefony/security", …)` propose les clés **et** les valeurs
177
+ d'enum (`referrerPolicy`, `coop`, `corp`…). Une valeur hors enum casse le boot, pas la production.
178
+
179
+ ### 2. Ce qu'on observe
180
+
181
+ ```bash
182
+ # Route applicative : socle transport + couche applicative, sur la MÊME réponse.
183
+ curl -sI http://localhost:5151/nodefony/test/index
184
+
185
+ # HTTP/1.1 200 OK
186
+ # X-Content-Type-Options: nosniff ← transport (@nodefony/http)
187
+ # X-Frame-Options: DENY ← transport (@nodefony/http)
188
+ # Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-vZ9…'; …
189
+ # Referrer-Policy: strict-origin-when-cross-origin
190
+ # Cross-Origin-Opener-Policy: same-origin
191
+ # Cross-Origin-Resource-Policy: same-origin
192
+ # Permissions-Policy: camera=(), microphone=(), geolocation=()
193
+ ```
194
+
195
+ ```bash
196
+ # Le nonce change à CHAQUE requête — deux appels, deux jetons.
197
+ curl -sI http://localhost:5151/nodefony/test/index | grep -o "nonce-[^']*"
198
+ curl -sI http://localhost:5151/nodefony/test/index | grep -o "nonce-[^']*"
199
+ # nonce-2Qk1r0h8… (≠)
200
+ # nonce-Xa7pLd3f…
201
+
202
+ # Une URL inexistante : le socle transport est TOUJOURS là (l'applicatif ne s'exécute pas).
203
+ curl -sI http://localhost:5151/nodefony/test/__inexistant__ | grep -i x-content-type
204
+ # X-Content-Type-Options: nosniff
205
+ ```
206
+
207
+ ### 3. Un besoin ponctuel : élargir le CSP d'UNE route
208
+
209
+ Tu dois embarquer une iframe YouTube sur une seule page. Élargir le CSP global serait une faute :
210
+ tu ouvrirais l'ensemble du site. `@Csp` déclare l'écart **à l'échelle de l'action**.
211
+
212
+ ```typescript
213
+ // nodefony/controllers/EmbedController.ts — complet, compile tel quel.
214
+ import { controller, Controller, Get, Csp } from "@nodefony/framework";
215
+
216
+ @controller("/embed")
217
+ class EmbedController extends Controller {
218
+ // `frame-src` n'existe QUE sur cette route ; le reste du site garde le CSP strict.
219
+ @Csp({ "frame-src": ["https://www.youtube.com"] })
220
+ @Get("/video")
221
+ video() {
222
+ return this.renderJson({ embed: true });
223
+ }
224
+ }
225
+
226
+ export default EmbedController;
227
+ ```
228
+
229
+ ```bash
230
+ curl -sI http://localhost:5151/embed/video | grep -i content-security-policy
231
+ # … ; frame-src https://www.youtube.com ← ajouté ICI seulement
232
+ curl -sI http://localhost:5151/nodefony/test/index | grep -c youtube
233
+ # 0 ← isolation prouvée (security-headers.test.ts:83)
234
+ ```
235
+
236
+ ## 🛡️ Le catalogue des en-têtes
237
+
238
+ Choisir en cinq secondes — puis le détail dans les cartes.
239
+
240
+ | En-tête | Ce qu'il bloque | Défaut Nodefony | Qui l'émet |
241
+ | ------------------------------ | ------------------------------------ | ------------------------------------------ | ------------------- |
242
+ | `Content-Security-Policy` | XSS, injection de source | politique « secure-but-usable » + nonce | security (pipeline) |
243
+ | `X-Frame-Options` | Clickjacking | `DENY` | http (transport) |
244
+ | `X-Content-Type-Options` | MIME sniffing | `nosniff` | http (transport) |
245
+ | `Strict-Transport-Security` | Downgrade HTTPS → HTTP | `max-age=31536000; includeSubDomains`, TLS | http (transport) |
246
+ | `Referrer-Policy` | Fuite d'URL vers des tiers | `no-referrer` | security (pipeline) |
247
+ | `Cross-Origin-Opener-Policy` | Attaques par fenêtre ouvrante | **absent** (opt-in) | security (pipeline) |
248
+ | `Cross-Origin-Embedder-Policy` | Chargement de ressources non signées | **absent** (opt-in) | security (pipeline) |
249
+ | `Cross-Origin-Resource-Policy` | Inclusion de tes assets par un tiers | **absent** (opt-in) | security (pipeline) |
250
+ | `Origin-Agent-Cluster` | Partage de heap entre origines | **absent** (opt-in) | security (pipeline) |
251
+ | `Permissions-Policy` | Accès caméra/micro/géoloc | **absent** (opt-in) | security (pipeline) |
252
+
253
+ ### `Content-Security-Policy` — la liste blanche des sources
254
+
255
+ **La menace** : n'importe quelle entrée non échappée (commentaire, nom d'utilisateur, paramètre
256
+ réfléchi) devient un vecteur d'exécution de code. Le CSP est le filet quand l'échappement a raté.
257
+
258
+ **Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts:218`) : seul `script-src`
259
+ est **strict** — `'self'` plus le nonce de la requête, ce qui est la vraie défense XSS. Le reste
260
+ couvre les besoins réels d'une app moderne (CSS-in-JS via `style-src 'unsafe-inline'`, images
261
+ `data:`/`blob:`, workers, fetch/WS same-origin) et ajoute les durcissements gratuits :
262
+ `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`.
263
+
264
+ **Pourquoi ce compromis** : un CSP qui casse l'application est désactivé par le premier développeur
265
+ pressé. Un CSP strict là où ça compte (`script-src`) et permissif là où ça ne coûte rien (styles,
266
+ images) survit en production — c'est celui-là qui protège vraiment.
267
+
268
+ Le CSP couvre aussi le clickjacking, via `frame-ancestors`, plus finement que `X-Frame-Options`
269
+ (liste d'origines plutôt que tout-ou-rien). Les deux cohabitent : les navigateurs modernes
270
+ privilégient `frame-ancestors`, `X-Frame-Options` reste le filet pour les anciens.
271
+
272
+ ### `X-Frame-Options` — non, tu ne m'encadres pas
273
+
274
+ **La menace** : le clickjacking. Ta page est superposée, invisible, à une page appât ; le clic de la
275
+ victime est capté par ton interface.
276
+
277
+ Posé par le **transport** depuis un cache calculé au boot — `secFrameOptions`
278
+ (`http-kernel.ts:270`) — et configuré côté `@nodefony/http` avec `frameOptions`
279
+ (`http/nodefony/config/config.ts:121`), qui vaut `DENY` par défaut. `SAMEORIGIN` si ton propre site
280
+ s'auto-encadre. C'est un des trois en-têtes que security **ne ré-émet pas** : il doit valoir aussi
281
+ pour un HTML statique servi directement depuis `public/`.
282
+
283
+ ### `X-Content-Type-Options` — arrête de deviner
284
+
285
+ **La menace** : le MIME sniffing. Un fichier téléversé, servi avec un `Content-Type` imprécis, est
286
+ « deviné » par le navigateur — et un fichier deviné exécutable s'exécute sur **ton** origine, donc
287
+ avec tes cookies.
288
+
289
+ Valeur unique reconnue : `nosniff`, posée depuis le cache `secContentTypeOptions`
290
+ (`http-kernel.ts:1334`). C'est **l'en-tête qui justifie le mieux la couche transport** : le danger
291
+ vient précisément des fichiers servis hors pipeline applicatif — un banc live le prouve sur une 404
292
+ (`security-headers.test.ts:38`).
293
+
294
+ ### `Strict-Transport-Security` — HTTPS, et rien d'autre
295
+
296
+ **La menace** : le downgrade. Sur un réseau hostile, la toute première requête en clair suffit à
297
+ installer un intercepteur.
298
+
299
+ La chaîne est assemblée au boot par `HttpKernel.computeSecurityHeaderCaches()`
300
+ (`http-kernel.ts:330`) : `max-age`, puis `includeSubDomains` et `preload` selon la config.
301
+
302
+ Elle n'est posée que **sur une réponse HTTPS ou HTTP/2** — le cache `secHsts` est conditionné au type
303
+ de serveur (`http-kernel.ts:965`). C'est conforme à la RFC 6797, qui veut qu'un HSTS reçu en clair
304
+ soit ignoré : l'émettre sur du HTTP simple ne ferait que polluer. Défaut : un an, sous-domaines
305
+ inclus.
306
+
307
+ > [!CAUTION]
308
+ > `preload: true` (`http/nodefony/config/config.ts:92`) inscrit ton domaine dans la liste
309
+ > pré-chargée des navigateurs. C'est un **engagement quasi irréversible** : tout sous-domaine
310
+ > incapable de servir en HTTPS devient inaccessible, et la sortie de liste prend des mois. À ne
311
+ > jamais activer « pour voir ».
312
+
313
+ ### `Referrer-Policy` — ne raconte pas d'où tu viens
314
+
315
+ **La menace** : la fuite d'URL. Chemins parlants, identifiants de session dans une query, jetons de
316
+ réinitialisation — tout part chez le site suivant via le `Referer`.
317
+
318
+ Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts:263`), la valeur la plus stricte. La valeur est un
319
+ **enum W3C fermé** — huit valeurs validées au boot, donc pas de faute de frappe qui passerait en
320
+ silence (l'écriture libre `no-refferer` casserait la protection sans prévenir).
321
+
322
+ Le choix usuel pour un site public reste `strict-origin-when-cross-origin` : URL complète en
323
+ interne, origine seule vers l'extérieur, rien du tout vers du HTTP en clair.
324
+
325
+ ### `Cross-Origin-Opener-Policy` — coupe le lien avec la fenêtre ouvrante
326
+
327
+ **La menace** : une page ouverte par la tienne (ou qui t'a ouverte) garde une référence
328
+ `window.opener` et partage un groupe de contexte de navigation — surface d'attaque pour du
329
+ _tabnabbing_ et pour les canaux auxiliaires type Spectre.
330
+
331
+ `same-origin` (`securityHeaders.ts:71`) rompt ce lien. C'est aussi, avec COEP, l'une des deux
332
+ conditions de l'**isolation cross-origin**, indispensable si tu veux `SharedArrayBuffer` ou des
333
+ timers haute résolution.
334
+
335
+ ### `Cross-Origin-Embedder-Policy` — je n'embarque que du consenti
336
+
337
+ **La menace** : ta page embarque des ressources tierces qui n'ont jamais donné leur accord, et les
338
+ place dans ton processus.
339
+
340
+ `require-corp` (`securityHeaders.ts:72`) exige que **chaque** ressource tierce s'annonce comme
341
+ partageable (CORP ou CORS). C'est le complément de COOP pour l'isolation complète.
342
+
343
+ > [!WARNING]
344
+ > `coep: "require-corp"` **casse toutes les ressources tierces non conformes** — polices Google,
345
+ > images de CDN, iframes de paiement. C'est pour cette raison qu'il est absent des défauts, et
346
+ > volontairement exclu du banc de test (`security-headers.test.ts:62`). À activer seulement si tu
347
+ > as besoin de l'isolation cross-origin, et après audit de tes assets.
348
+
349
+ ### `Cross-Origin-Resource-Policy` — mes assets ne s'incluent pas ailleurs
350
+
351
+ **La menace** : symétrique du précédent. Un site tiers inclut tes images ou tes scripts pour les
352
+ mesurer, les mettre en cache, ou monter une attaque par inclusion.
353
+
354
+ `same-origin` (`securityHeaders.ts:73`) interdit toute inclusion externe ; `same-site` autorise tes
355
+ propres sous-domaines ; `cross-origin` ouvre — c'est ce qu'il faut sur une CDN publique assumée.
356
+
357
+ ### `Origin-Agent-Cluster` — un bac à sable par origine
358
+
359
+ **La menace** : plusieurs origines partageant heap et processus, donc des canaux auxiliaires
360
+ mesurables.
361
+
362
+ Nodefony l'émet comme un **booléen de champ structuré** RFC 8941 : la valeur est littéralement `?1`
363
+ (`securityHeaders.ts:75`). C'est une **demande**, pas une garantie — le navigateur décide.
364
+
365
+ ### `Permissions-Policy` — coupe le micro par défaut
366
+
367
+ **La menace** : une iframe tierce (widget, publicité) demande la caméra, le micro ou la position, et
368
+ la boîte de dialogue s'affiche sous **ton** nom de domaine.
369
+
370
+ Valeur libre — le champ `permissionsPolicy` est recopié tel quel (`securityHeaders.ts:76`),
371
+ typiquement `camera=(), microphone=(), geolocation=()` : la
372
+ liste vide signifie « personne, pas même moi ». Absent par défaut car la liste des fonctionnalités
373
+ dépend entièrement de l'application.
374
+
375
+ ## ⚙️ Configuration — deux sections, deux modules
376
+
377
+ Réflexe à acquérir : **le nom du module dit qui pose l'en-tête**. Chercher `frameOptions` dans la
378
+ config security est la première source de confusion sur ce sujet.
379
+
380
+ ### Couche applicative — `use("@nodefony/security", { headers })`
381
+
382
+ Dérivé du schéma Zod `headersSchema` (`config.ts:194`).
383
+
384
+ <!-- prettier-ignore -->
385
+ | Option | Type | Défaut | Effet |
386
+ | --- | --- | --- | --- |
387
+ | `enabled` | booléen | `true` | Coupe toute la couche applicative. |
388
+ | `csp` | chaîne | politique « secure-but-usable » | Valeur de `Content-Security-Policy`. |
389
+ | `cspNonces` | booléen | `true` | Active la substitution de `{{nonce}}` par requête. |
390
+ | `referrerPolicy` | enum W3C (8 valeurs) | `no-referrer` | Valeur de `Referrer-Policy`. |
391
+ | `coop` | enum, optionnel | absent | `Cross-Origin-Opener-Policy`. |
392
+ | `coep` | enum, optionnel | absent | `Cross-Origin-Embedder-Policy`. |
393
+ | `corp` | enum, optionnel | absent | `Cross-Origin-Resource-Policy`. |
394
+ | `originAgentCluster` | booléen, optionnel | absent | Émet `Origin-Agent-Cluster: ?1`. |
395
+ | `permissionsPolicy` | chaîne, optionnelle | absent | Valeur de `Permissions-Policy`. |
396
+
397
+ ### Socle transport — `use("@nodefony/http", { securityHeaders })`
398
+
399
+ Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts:108`). Ces trois réglages sont
400
+ **éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts:290`)
401
+ recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
402
+
403
+ | Option | Type | Défaut | Effet |
404
+ | ------------------------------------------- | ----------------- | ---------- | ------------------------------------------------------------------ |
405
+ | `contentTypeOptions` | chaîne ou `null` | `nosniff` | `X-Content-Type-Options` ; `null` = ne pas émettre. |
406
+ | `frameOptions` | chaîne ou `null` | `DENY` | `X-Frame-Options` ; `SAMEORIGIN` si auto-encadrement. |
407
+ | `strictTransportSecurity` | objet ou `null` | activé | `null` = pas de HSTS du tout. |
408
+ | `strictTransportSecurity.maxAge` | entier (secondes) | `31536000` | Durée mémorisée par le navigateur (un an, recommandé OWASP). |
409
+ | `strictTransportSecurity.includeSubDomains` | booléen | `true` | Étend la contrainte à tous les sous-domaines. |
410
+ | `strictTransportSecurity.preload` | booléen | `false` | Inscription à la liste pré-chargée — **irréversible en pratique**. |
411
+
412
+ > [!WARNING]
413
+ > Les clés `hsts`, `hstsMaxAgeS`, `frameguard` et `noSniff` **existent** dans la config security
414
+ > (`config.ts:200`, `config.ts:227`, `config.ts:233`) mais **ne pilotent rien** : la couche
415
+ > applicative ne les lit pas (`securityHeaders.ts:6`), elles ne servent qu'à l'introspection
416
+ > affichée dans Studio. Pour changer réellement `X-Frame-Options`, c'est `securityHeaders.frameOptions`
417
+ > **du module http**. Même remarque pour `hidePoweredBy` (`config.ts:254`) : Nodefony n'émet aucun
418
+ > `X-Powered-By`, l'option est un no-op documenté.
419
+
420
+ ## 🏗️ Le CSP en détail — deux régimes, trois façons de l'étendre
421
+
422
+ ### Régime 1 — CSP statique (0 allocation par requête)
423
+
424
+ Si le CSP ne contient pas `{{nonce}}`, ou si `cspNonces` est à `false`, la chaîne est rangée telle
425
+ quelle dans la table gelée du boot (`securityHeaders.ts:66`). Par requête : une lecture, un
426
+ `setHeader`. Rien d'autre.
427
+
428
+ Détail de robustesse : si tu désactives `cspNonces` en laissant le placeholder dans la chaîne, le
429
+ token résiduel `'nonce-{{nonce}}'` est **purgé** (`securityHeaders.ts:63`). Sans ça, tu servirais un
430
+ CSP contenant un nonce littéral jamais émis — donc un `script-src` qui bloque **tout**, y compris tes
431
+ propres scripts. Le code refuse de produire un CSP cassé.
432
+
433
+ ### Régime 2 — nonce par requête (la vraie défense anti-XSS)
434
+
435
+ Un nonce autorise **l'inline que tu as toi-même rendu**, et lui seul. Un script injecté par un
436
+ attaquant ne peut pas deviner la valeur : il est refusé même s'il est syntaxiquement identique.
437
+
438
+ Le chemin complet, sans surprise :
439
+
440
+ 1. **Au boot**, la chaîne CSP est **pré-découpée** autour de `{{nonce}}` (`securityHeaders.ts:58`).
441
+ Aucun parsing ni regex n'aura lieu pendant une requête.
442
+ 2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts:835`) lit `context.cspNonce` —
443
+ ce qui **génère** le jeton à cet instant (`Context.ts:253`) — puis appelle
444
+ `SecurityHeaders.cspFor()` (`securityHeaders.ts:100`) : un seul `join`.
445
+ 3. **Dans la vue**, le contrôleur relit `context.cspNonce`, qui est **mémoïsé** : l'en-tête et le
446
+ `<script nonce="…">` portent forcément la même valeur. C'est le motif employé par le contrôleur
447
+ de Studio (`StudioController.ts:62`).
448
+
449
+ `SecurityHeaders.hasNonce` (`securityHeaders.ts:88`) est le drapeau qui décide du régime : à `false`,
450
+ pas une seule opération crypto. Il n'y a **aucun setter** pour `cspNonce` : un jeton serveur doit
451
+ rester imprévisible, jamais pilotable par le client — contrairement au `requestId`, qui, lui, accepte
452
+ une corrélation entrante.
453
+
454
+ **Placement dans le pipeline** : `applySecurityHeaders` est appelé **après le resolve** et **avant**
455
+ le repli statique et le `writeHead` (`http-kernel.ts:1334`). Cet ordre n'est pas cosmétique : il
456
+ faut que le routeur ait posé les directives `@Csp` de la route pour pouvoir les fusionner, et il faut
457
+ être avant l'écriture des en-têtes pour pouvoir en poser.
458
+
459
+ ### Étendre le CSP — trois portées, une seule mécanique
460
+
461
+ | Portée | Outil | Pour quoi | Recalculé |
462
+ | ------------------ | ------------------------------- | ----------------------------------------------- | ------------------- |
463
+ | Application | `headers.csp` en config | ta politique de base | au boot |
464
+ | Module | `Firewall.registerCspOrigins()` | besoin **permanent** d'un module (ex. Vite) | à l'enregistrement |
465
+ | Route / contrôleur | `@Csp({ … })` | besoin **ponctuel** d'une réponse (iframe, CDN) | par requête décorée |
466
+
467
+ Les trois convergent vers `mergeCspFragments()` (`csp.ts:56`), et c'est un **merge structuré**, pas
468
+ une concaténation. Pourquoi c'est vital : en CSP, une directive **répétée est ignorée** après sa
469
+ première occurrence (W3C CSP Level 3 §3, rationnel documenté `csp.ts:8`). Concaténer
470
+ `"script-src 'self'"` et `"script-src https://cdn"` produirait un en-tête où la seconde est purement
471
+ et simplement jetée — une extension silencieusement sans effet, le pire des deux mondes.
472
+
473
+ Le merge fusionne donc les sources **dans une seule directive**, dédoublonnées, base d'abord ; une
474
+ directive absente est ajoutée en fin. La fonction est **pure et déterministe** (`parseCsp()`
475
+ `csp.ts:25` → `serializeCsp()` `csp.ts:34`), ce qui rend l'en-tête stable d'une requête à l'autre et
476
+ les tests fiables.
477
+
478
+ **Coût** : le merge d'un module est payé **une fois**, au (dés)enregistrement
479
+ (`Firewall.#rebuildSecurityHeaders()`, `firewall.ts:1085`), jamais par requête. Le merge d'une route
480
+ `@Csp` est payé **uniquement sur les routes décorées** (`SecurityHeaders.cspForExtra()`,
481
+ `securityHeaders.ts:115`) ; le cas courant reste le simple `join`.
482
+
483
+ ## 🧩 Extension — déclarer un fragment CSP depuis son module
484
+
485
+ Un module qui a besoin d'origines supplémentaires ne doit **jamais** poser l'en-tête lui-même : il en
486
+ émettrait un second, et le navigateur applique alors l'intersection la plus stricte — au mieux
487
+ inefficace, au pire il casse la page. Il **déclare** son besoin, security fusionne.
488
+
489
+ ```typescript
490
+ // Dans le service d'un module — le firewall est résolu PAR NOM (aucun import de security).
491
+ const firewall = this.container?.get?.("firewall") as
492
+ | { registerCspOrigins?(m: string, f: Record<string, string[]>): void }
493
+ | undefined;
494
+
495
+ firewall?.registerCspOrigins?.("mon-module", {
496
+ "connect-src": ["https://api.partenaire.tld"],
497
+ "img-src": ["https://cdn.partenaire.tld"],
498
+ });
499
+ ```
500
+
501
+ Trois propriétés à retenir :
502
+
503
+ - **Aucun couplage** : la résolution par nom de service évite un cycle de dépendances, et
504
+ `registerCspOrigins` est optionnel — un module fonctionne dans une app **sans** security.
505
+ - **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts:1074`) retire le fragment et
506
+ reconstruit le CSP de base. C'est ce que fait `@nodefony/frontend` à l'arrêt du serveur Vite.
507
+ - **Idempotent** : la reconstruction repart **toujours** du `headers.csp` d'origine
508
+ (`firewall.ts:1088`), jamais d'un CSP déjà fusionné — pas d'accumulation entre deux
509
+ enregistrements.
510
+
511
+ L'exemple de référence vit dans le framework : en développement, `@nodefony/frontend` déclare les
512
+ origines du serveur Vite et `'unsafe-eval'` (exigé par le Fast Refresh de React) via
513
+ `FrontendService.#viteCspFragment()` (`FrontendService.ts:909`) — ce qui explique qu'un CSP observé
514
+ en dev soit plus large qu'en production, où ce fragment n'existe pas.
515
+
516
+ ## 📜 Normes appliquées
517
+
518
+ | Domaine | Norme | Ancrage dans le code |
519
+ | ------------------------------------ | -------------------------------- | ------------------------------------------------------------ |
520
+ | Politique de sécurité du contenu | W3C CSP Level 3 | `mergeCspFragments()` (`csp.ts:56`), directive non dupliquée |
521
+ | Nonce CSP (unicité, imprévisibilité) | W3C CSP Level 3 §6.7.4 | `Context.cspNonce` — 128 bits CSPRNG (`Context.ts:253`) |
522
+ | HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts:839`) |
523
+ | Champ structuré booléen | RFC 8941 | `Origin-Agent-Cluster: ?1` (`securityHeaders.ts:75`) |
524
+ | Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts:239`) |
525
+ | Isolation cross-origin | WHATWG HTML (COOP/COEP/CORP) | `securityHeaders.ts:71` |
526
+ | Anti-MIME-sniffing | WHATWG Fetch (`nosniff`) | `secContentTypeOptions` (`http-kernel.ts:1334`) |
527
+ | Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts:330`) |
528
+
529
+ ## ⚡ Performance & mémoire
530
+
531
+ Le coût est concentré au boot, par construction :
532
+
533
+ - **En-têtes constants** : une seule table, gelée (`securityHeaders.ts:77`). Par requête, une boucle
534
+ `for…in` sur un objet de 1 à 6 entrées et autant de `setHeader`. Aucune allocation.
535
+ - **CSP statique** : rien de plus — la chaîne est dans la table.
536
+ - **CSP à nonce** : `randomBytes(16)` plus un `join` par requête. C'est le seul coût variable, et il
537
+ n'existe **que** si le CSP porte un placeholder : `hasNonce` (`securityHeaders.ts:88`)
538
+ court-circuite entièrement ce chemin sinon. La paresse de `Context.cspNonce` (`Context.ts:253`)
539
+ protège en plus les chemins internes qui n'atteignent jamais le firewall.
540
+ - **Merge CSP** : jamais dans le chemin chaud. Le fragment d'un module est fusionné à
541
+ l'enregistrement (`firewall.ts:1067`) ; celui d'une route ne coûte que sur les routes `@Csp`.
542
+ - **Socle transport** : trois `setHeader` sur des chaînes précalculées (`http-kernel.ts:1334`), avec
543
+ un test `!== null` qui annule le coût des en-têtes désactivés.
544
+
545
+ Le module n'attache aucun écouteur d'événement et ne conserve aucun état par requête : il n'entre pas
546
+ dans le périmètre du gate mémoire, qu'il ne peut structurellement pas dégrader.
547
+
548
+ ## 📡 Observabilité — Studio
549
+
550
+ L'écran **Firewall** de Studio affiche la section « En-têtes de sécurité » — pilotée par
551
+ `headers.enabled` (`FirewallDefenses.tsx:219`) — avec le CSP effectif, l'état du nonce par requête, la
552
+ Referrer-Policy et les valeurs d'isolation. Les données
553
+ viennent de `Firewall.describe()` (`firewall.ts:505`), qui projette la config **sans aucun secret**,
554
+ exposée par `GET /nodefony/security/api/firewall`.
555
+
556
+ L'onglet **Configuration** de Studio rend les mêmes options depuis le schéma Zod — chaque champ y
557
+ porte sa description, ce qui en fait la référence toujours à jour des défauts.
558
+
559
+ > [!NOTE]
560
+ > La ligne « frameguard » de cet écran reflète la **config security**, pas la valeur réellement émise
561
+ > par le transport. La source de vérité pour `X-Frame-Options`, c'est la réponse HTTP elle-même :
562
+ > `curl -I` tranche en une seconde.
563
+
564
+ ## ⚠️ Pièges (symptôme → cause → correction)
565
+
566
+ | Symptôme | Cause dans le code | Correction |
567
+ | -------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
568
+ | `frameguard: "sameorigin"` en config security sans effet | Clé **inerte** — non lue par la couche applicative (`securityHeaders.ts:6`) | Régler `securityHeaders.frameOptions` du module **http** |
569
+ | `<script>` inline bloqué | Le template ne reprend pas le nonce | Rendre `<script nonce="…">` depuis `context.cspNonce` |
570
+ | CSP contenant `'nonce-{{nonce}}'` littéral | `cspNonces` désactivé, placeholder laissé | Nodefony purge le résiduel (`securityHeaders.ts:63`) ; retirer le placeholder |
571
+ | Une extension `script-src` de module sans effet | Second en-tête / directive dupliquée (ignorée, W3C CSP3 §3) | Déclarer un fragment via `registerCspOrigins()`, jamais un `setHeader` |
572
+ | Assets tiers cassés après activation de l'isolation | `coep: "require-corp"` exige CORP/CORS sur **chaque** ressource | Retirer `coep` ou faire annoncer les ressources |
573
+ | HSTS absent en développement | Posé sur TLS uniquement (`http-kernel.ts:839`) | Comportement conforme RFC 6797 — vérifier sur le port HTTPS |
574
+ | Le CSP est plus large en dev qu'en prod | `@nodefony/frontend` déclare les origines Vite (`FrontendService.ts:695`) | Attendu : le fragment n'existe pas en production |
575
+ | Aucun en-tête applicatif | Module security absent ou `headers.enabled: false` | Le socle transport reste actif ; réactiver security pour CSP/Referrer/isolation |
576
+ | Domaine injoignable après activation de `preload` | Inscription à la liste pré-chargée, sortie très lente | Ne jamais activer sans plan HTTPS sur **tous** les sous-domaines |
577
+
578
+ ## 🧪 Tests & couverture
579
+
580
+ Trois familles couvrent la brique — les compteurs exacts vivent dans la carte de l'aperçu, régénérée
581
+ depuis vitest :
582
+
583
+ - **Unitaires** (`@nodefony/security`) — `securityHeaders.test` : la table figée, la séparation
584
+ transport/applicatif prouvée par l'absence des trois en-têtes transport, les avancés opt-in, les
585
+ deux régimes CSP, la purge du résiduel, et `cspForExtra` (fusion, ajout, substitution du nonce) ;
586
+ `csp.test` : parse, merge et sérialisation des fragments.
587
+ - **Intégration sur serveur réel** (`@nodefony/http`, port 5151) — `security-headers.test` vérifie les
588
+ deux couches sur une **vraie** réponse. Quatre invariants y sont prouvés :
589
+ - le socle transport survit à une 404, avec `x-content-type-options` sur une route inexistante
590
+ (`security-headers.test.ts:38`) ;
591
+ - le CSP applicatif complète cette réponse avec `content-security-policy`
592
+ (`security-headers.test.ts:43`) ;
593
+ - une route décorée voit sa directive `frame-src` fusionnée, sans dupliquer `img-src`
594
+ (`security-headers.test.ts:73`) ;
595
+ - deux requêtes concurrentes reçoivent deux nonces différents (`security-headers.test.ts:100`).
596
+
597
+ `headers.test` et `security.test` couvrent le reste du contrat d'en-têtes.
598
+
599
+ - **Absent, assumé** : pas de banc d'**attaque** dédié à cette brique (contrairement à CSRF, CORS ou
600
+ l'autorisation, qui ont leur `*.attack.test.ts`), et pas de test de **charge** propre — le coût est
601
+ structurellement nul par requête, et le pipeline complet est déjà couvert par le gate mémoire de
602
+ `@nodefony/http`.
603
+
604
+ Couverture : `npm run coverage` dans `@nodefony/security`. Revue de sécurité d'un diff touchant cette
605
+ brique : skill `nodefony-security-review`.
606
+
607
+ ## 🔗 Pour aller plus loin
608
+
609
+ - ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
610
+ - 🧭 **Pages sœurs** : [CORS](cors.md) · [CSRF](csrf.md)
611
+
612
+ - Le pare-feu qui pose la couche applicative → [firewall](./firewall.md)
613
+ - CORS, l'autre famille d'en-têtes (`Access-Control-*`) → [cors](./cors.md)
614
+ - CSRF, la défense complémentaire contre les requêtes forcées → [csrf](./csrf.md)
615
+ - Vue d'ensemble du module → [index](./index.md)
616
+ - Où les deux couches s'insèrent dans le pipeline → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)