@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,132 @@
1
+ import { type Container } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
+ import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
5
+ import type { IToken } from "../../contracts/IToken.js";
6
+ import { type ExternalSubjectMapping } from "./externalSubject.js";
7
+ /** Comment le sujet d'un jeton devient un utilisateur de cette application. */
8
+ export type ExternalSubjectPolicy = "require" | "ephemeral";
9
+ /** Un émetteur reconnu, et la façon dont ses sujets entrent chez nous. */
10
+ export interface IExternalIssuerBinding {
11
+ /** Émetteur de confiance, sous sa forme canonique. */
12
+ issuer: string;
13
+ /** Comment le `sub` de CET émetteur devient un identifiant local. */
14
+ subjectMapping: ExternalSubjectMapping;
15
+ }
16
+ /** Ce que la fabrique doit fournir à l'authenticator. */
17
+ export interface IExternalJwtAuthenticatorOptions {
18
+ /**
19
+ * Émetteurs de confiance, et leur politique de sujet.
20
+ *
21
+ * La liste sert à deux choses, qu'il ne faut pas confondre : reconnaître les
22
+ * jetons qui relèvent de cet authenticator (aiguillage — elle n'accorde
23
+ * rien, le vérificateur refait le contrôle sur sa propre liste), et savoir
24
+ * dans quel espace de noms lire le sujet de chacun.
25
+ */
26
+ issuers: readonly IExternalIssuerBinding[];
27
+ /** Politique de rattachement du sujet à un utilisateur local. */
28
+ subjectPolicy: ExternalSubjectPolicy;
29
+ /** Rôles accordés en mode `ephemeral`. */
30
+ ephemeralRoles: readonly string[];
31
+ }
32
+ /**
33
+ * Authentification par **jeton d'accès émis par un serveur d'autorisation
34
+ * TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).
35
+ *
36
+ * C'est le chaînon qui relie deux pièces déjà en place : le vérificateur de
37
+ * jetons distants, qui sait lire un jeton dont on ne possède pas la clé, et le
38
+ * pare-feu, qui raisonne en utilisateurs et en rôles. Le vérificateur s'arrête
39
+ * à un sujet et des scopes — délibérément, car établir une identité
40
+ * applicative est une décision de l'application, pas du protocole. C'est cette
41
+ * décision-là que porte cette classe, et rien d'autre.
42
+ *
43
+ * ## Cohabitation avec les jetons maison
44
+ *
45
+ * `JwtAuthenticator` et celui-ci reconnaissent la même forme de credential.
46
+ * Chacun ne prend donc que les jetons dont l'émetteur revendiqué est le sien
47
+ * ({@link peekIssuer}) — lecture non vérifiée qui ne sert qu'à AIGUILLER. Sans
48
+ * cela, en mode `first`, le premier listé capturerait les deux familles et
49
+ * refuserait la moitié des jetons : l'ordre de la configuration deviendrait
50
+ * une décision de sécurité, dont l'erreur ne se verrait qu'en production.
51
+ *
52
+ * ## Ce qui vaut garantie
53
+ *
54
+ * - **L'audience vient de la ZONE**, jamais du jeton, et elle est obligatoire :
55
+ * sans elle l'authenticator refuse de démarrer ({@link validateArea}).
56
+ * - **Un refus est un 401 uniforme ; une PANNE est un 503** — un émetteur
57
+ * injoignable n'est pas un jeton invalide, et le dire autrement enverrait le
58
+ * client renouveler en boucle un jeton parfaitement bon.
59
+ * - **Le sujet est revérifié localement** en mode `require` : un compte
60
+ * supprimé, désactivé ou verrouillé ferme l'accès sans attendre l'expiration
61
+ * du jeton, que l'application ne contrôle pas.
62
+ *
63
+ * - **Le sujet n'entre jamais nu dans l'espace de noms local** : un `sub` n'est
64
+ * unique que chez son émetteur, et le rattachement passe donc par
65
+ * {@link localIdentifierFor}, piloté par le `subjectMapping` de CET émetteur.
66
+ * - **Le refus est apprenable** : le défi porte le pointeur `resource_metadata`
67
+ * (RFC 9728), qui dit au client où aller chercher de quoi obtenir un jeton.
68
+ */
69
+ export declare class ExternalJwtAuthenticator implements IAuthenticator {
70
+ #private;
71
+ readonly name = "external-jwt";
72
+ /**
73
+ * @param container - container DI (résolution lazy du vérificateur et de `users`)
74
+ * @param options - émetteurs reconnus et politique de rattachement
75
+ */
76
+ constructor(container: Container, options: IExternalJwtAuthenticatorOptions);
77
+ /**
78
+ * La requête porte-t-elle un jeton se réclamant d'un émetteur de confiance ?
79
+ *
80
+ * Le contrôle est délibérément le MÊME que celui du vérificateur, et non un
81
+ * simple « c'est un JWT » : un jeton maison ne doit pas être capturé ici, et
82
+ * un jeton d'un émetteur inconnu n'a pas à provoquer le moindre travail.
83
+ */
84
+ supports(context: ContextType): boolean;
85
+ /**
86
+ * Extrait le jeton brut ET l'audience de la zone.
87
+ *
88
+ * L'audience transite par le token parce que `authenticate()` ne reçoit pas
89
+ * le contexte : c'est ici, et seulement ici, qu'on sait quelle ressource est
90
+ * visée.
91
+ */
92
+ createToken(context: ContextType): Promise<IToken>;
93
+ /**
94
+ * Vérifie le jeton auprès de son émetteur, puis rattache le sujet.
95
+ *
96
+ * @throws AuthenticationError (401) — jeton refusé, ou sujet sans compte
97
+ * local utilisable
98
+ * @throws UnverifiableTokenError (503) — rien ne peut vérifier ce jeton, ou
99
+ * l'émetteur est injoignable : on ne sait pas, et on le dit
100
+ */
101
+ authenticate(token: IToken): Promise<IToken>;
102
+ /** Slot audit — le firewall enregistre déjà succès et échec par zone. */
103
+ onSuccess(_context: ContextType, _token: IToken): Promise<void>;
104
+ /** Slot audit — le 401 et le défi sont posés par le firewall. */
105
+ onFailure(_context: ContextType, _error: Error): Promise<void>;
106
+ /**
107
+ * Défi RFC 6750 + pointeur RFC 9728, posé par le firewall sur les 401.
108
+ *
109
+ * ⭐ **C'est cet en-tête qui rend l'autorisation apprenable.** Un `Bearer` nu
110
+ * est un mur : le client sait qu'il lui faut un jeton, mais pas où le
111
+ * demander. `resource_metadata` nomme le document qui le lui dira — c'est le
112
+ * seul mécanisme normalisé pour ça, et celui qu'un client MCP conforme suit.
113
+ *
114
+ * Aucun `error` n'est joint : le firewall pose ce défi sur TOUT 401 de la
115
+ * zone, sans savoir si la requête portait un jeton. Or la RFC 6750 §3
116
+ * demande de ne PAS mettre de code d'erreur quand elle n'en portait aucun —
117
+ * un `invalid_token` ferait renouveler en boucle un jeton qui n'existe pas.
118
+ *
119
+ * @param area - zone refusante ; sans elle (ou sans ressource déclarée) le
120
+ * défi retombe sur `Bearer` nu, faute de ressource à nommer
121
+ */
122
+ challenge(area?: ISecuredArea): string;
123
+ /**
124
+ * Refuse une zone sans ressource — au boot, pas à la première requête.
125
+ *
126
+ * Sans audience, la vérification accepterait un jeton émis pour un autre
127
+ * service : le seul verrou qui lie un jeton à CE service disparaîtrait, et
128
+ * l'application n'en saurait rien.
129
+ */
130
+ validateArea(area: ISecuredArea): void;
131
+ }
132
+ export default ExternalJwtAuthenticator;
@@ -0,0 +1,78 @@
1
+ import type { IRealtimeAuthenticator, IRealtimeHandshake, IRealtimeToken } from "../realtime/realtimeContracts.js";
2
+ /**
3
+ * Authenticator realtime des identités résolues par le **firewall** — équivalent
4
+ * WS de tout ce que le pipeline HTTP sait authentifier.
5
+ *
6
+ * ── Pourquoi il NE re-lit PAS la base ──────────────────────────────────────
7
+ * Un handshake WebSocket est une requête upgrade HTTP qui traverse le MÊME
8
+ * pipeline : `startSession` (reprise L1 du cookie) **puis** `firewall.handleSecurity`
9
+ * tournent AVANT que le `RealtimeController` ne fasse son handshake. Sur une zone
10
+ * data plane, le firewall a donc DÉJÀ : (1) authentifié (session, JWT, clé API…),
11
+ * (2) re-résolu l'identité via le provider `users` (rôles frais), (3) posé
12
+ * l'`IUser` **et le jeton** dans l'ALS et appliqué le Zero Trust (un anonyme est
13
+ * fermé AVANT d'arriver ici). Re-décoder le credential ici referait des lectures
14
+ * base **redondantes** par connexion — sur le différenciateur temps réel, un coût
15
+ * évitable. → on **réutilise** l'identité déjà en ALS.
16
+ *
17
+ * Le `RealtimeController.onHandshake` s'exécute dans la même bulle ALS que le
18
+ * firewall (un seul `RequestContext.run` enveloppe handshake + frames) → la
19
+ * lecture est sûre et synchrone.
20
+ *
21
+ * ── Il n'est PAS l'authenticator « de la session » ──────────────────────────
22
+ * Son nom d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode
23
+ * branché, pas son rôle : il promeut **toute** identité que le firewall a posée,
24
+ * y compris un agent authentifié par jeton porteur, sans cookie ni session. La
25
+ * confusion a coûté cher — un durcissement pensé pour la session a été appliqué
26
+ * à toutes les identités, et une connexion JWT parfaitement valide se faisait
27
+ * révoquer au motif qu'elle n'avait pas de session. D'où le nom actuel : il dit
28
+ * d'où vient l'identité (le firewall), pas comment elle a été prouvée.
29
+ *
30
+ * ── Révocation : un invariant, deux preuves ────────────────────────────────
31
+ * L'invariant est unique — **une socket ne survit pas à l'identité qui l'a
32
+ * ouverte** — mais la preuve dépend du mode, parce que ce sont deux mécanismes
33
+ * de révocation différents :
34
+ *
35
+ * | Mode | Ce qui rend l'identité morte |
36
+ * | -------------------------- | ------------------------------------------------ |
37
+ * | session BFF (`session`) | session détruite, expirée, ou passée à un autre |
38
+ * | jeton porteur (JWT, clé…) | `exp` atteint · `jti` denylisté · `invalidBefore` |
39
+ *
40
+ * Le jeton est figé au handshake (les frames lisent un cache O(1), jamais la
41
+ * base) ; la re-validation tourne sur le tick du hub (`REVOCATION_REVALIDATE_MS`)
42
+ * et devant chaque `api.request`. Une révocation prend donc effet en une fenêtre,
43
+ * pas à la frame suivante — c'est l'état de l'art (Socket.IO/Phoenix figent aussi
44
+ * l'identité au handshake).
45
+ */
46
+ export declare class FirewallRealtimeAuthenticator implements IRealtimeAuthenticator {
47
+ #private;
48
+ readonly name = "firewall-realtime";
49
+ /**
50
+ * @param resolveStore - fournit le store de révocation des jetons (le firewall
51
+ * passe une closure sur son container). Omis → mode dégradé documenté :
52
+ * seules les bornes portées par le jeton lui-même sont vérifiables.
53
+ */
54
+ constructor(resolveStore?: (() => IRealtimeRevocationStore | null) | null);
55
+ /** Une identité authentifiée a-t-elle été résolue (par le firewall) au handshake ? */
56
+ supports(_handshake: IRealtimeHandshake): boolean;
57
+ /**
58
+ * Promeut l'identité déjà résolue (ALS) en jeton realtime — 0 lecture base.
59
+ *
60
+ * @throws AuthenticationError — aucune identité authentifiée en ALS (ne devrait
61
+ * pas arriver sur une zone data plane : le firewall ferme l'anonyme en amont ;
62
+ * filet défensif fail-closed → le hub ferme la socket en 4001).
63
+ */
64
+ authenticate(_handshake: IRealtimeHandshake): Promise<IRealtimeToken>;
65
+ }
66
+ /**
67
+ * Surface MINIMALE du store de jetons consommée ici : les deux lectures qui
68
+ * disent si un jeton a été révoqué avant son terme. Typée localement plutôt
69
+ * qu'importée d'`ITokenStore` — ce module n'a besoin ni du reste du contrat ni
70
+ * du couplage, et un test peut fournir un double en deux lignes.
71
+ */
72
+ export interface IRealtimeRevocationStore {
73
+ /** `true` si ce `jti` a été mis sur la denylist et n'est pas encore expiré. */
74
+ isJtiDenied(jti: string): Promise<boolean>;
75
+ /** Seuil de révocation en masse du porteur (epoch ms), ou `null`. */
76
+ getInvalidBefore(subjectId: string): Promise<number | null>;
77
+ }
78
+ export default FirewallRealtimeAuthenticator;
@@ -0,0 +1,69 @@
1
+ import type { Container } from "nodefony";
2
+ import type { ContextType } from "@nodefony/http";
3
+ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
+ import type { IToken } from "../../contracts/IToken.js";
5
+ import type { IJwtRuntime } from "../token/jwtRuntime.js";
6
+ /**
7
+ * Authentification par **JWT Bearer** (RFC 6750) — réservée API service↔service /
8
+ * agents (le web utilise la session BFF). Vérifie un access token EdDSA signé par
9
+ * le {@link IJwtKeystore} du serveur.
10
+ *
11
+ * Défenses **dures** (RFC 8725 JWT BCP, prouvées en test) :
12
+ * - **allowlist d'algorithmes** côté serveur (`["EdDSA"]`) — l'algo n'est JAMAIS
13
+ * choisi d'après l'en-tête du token (§3.1) ; `alg=none` jamais accepté par jose.
14
+ * - **clé par `kid` depuis le keyset LOCAL** (`createLocalJWKSet`) — jamais
15
+ * `jku`/`jwk` de l'en-tête (injection de clé / SSRF, §3.5).
16
+ * - **`aud` (§3.9) + `iss` (§3.8) obligatoires** + `typ:"at+jwt"` (§3.11, sépare
17
+ * access et refresh) + `exp`/`nbf` (jose).
18
+ * - **révocation** : denylist `jti` + seuil `invalidBefore` par porteur (le JWT
19
+ * est auto-porté et non révocable sans état serveur).
20
+ * - **sujet revérifié** (§3.10) : `loadUserByIdentifier(sub)` → compte disparu,
21
+ * inactif ou verrouillé = rejet.
22
+ *
23
+ * Dépendances (keystore, store, userProvider) résolues **paresseusement** du
24
+ * container au premier usage (cold path) ; jose importé **lazy** (dep lourde).
25
+ */
26
+ export declare class JwtAuthenticator implements IAuthenticator {
27
+ #private;
28
+ readonly name = "jwt";
29
+ /**
30
+ * @param container - container DI (résolution lazy de `jwtKeystore`/`tokenStore`/`users`).
31
+ * @param runtime - paramètres JWT effectifs (iss/aud/ttl) partagés avec l'émetteur.
32
+ */
33
+ constructor(container: Container, runtime: IJwtRuntime);
34
+ /**
35
+ * La requête porte-t-elle un `Authorization: Bearer <jws>` émis par NOUS ?
36
+ *
37
+ * L'émetteur revendiqué est lu sans être vérifié ({@link peekIssuer}) et sert
38
+ * uniquement à AIGUILLER : `ExternalJwtAuthenticator` reconnaît la même forme
39
+ * de credential pour les jetons d'un serveur d'autorisation tiers. Sans ce
40
+ * discriminant, en mode `first`, le premier des deux listés dans la zone
41
+ * capturerait les deux familles et refuserait la moitié des jetons — l'ordre
42
+ * de la configuration deviendrait une décision de sécurité, dont l'erreur ne
43
+ * se verrait qu'en production.
44
+ *
45
+ * Un jeton dont l'émetteur est illisible reste pris en charge ici : c'est un
46
+ * jeton maison malformé, que la vérification refusera en le disant, plutôt
47
+ * qu'un credential qui disparaîtrait sans laisser de trace.
48
+ */
49
+ supports(context: ContextType): boolean;
50
+ /** Extrait le token brut (non vérifié) → porté par un `UserToken` type `"jwt"`. */
51
+ createToken(context: ContextType): Promise<IToken>;
52
+ /**
53
+ * Vérifie la signature + les claims du JWT, applique la révocation et résout le
54
+ * sujet — ou lève un 401 au message uniforme.
55
+ *
56
+ * @throws AuthenticationError (401) — token absent/invalide/expiré/révoqué, ou
57
+ * sujet disparu/banni.
58
+ * @throws Error (câblage : keystore/store/users absents) — logguée ERROR par le
59
+ * firewall puis 401 fail-closed (rien ne fuite au client).
60
+ */
61
+ authenticate(token: IToken): Promise<IToken>;
62
+ /** Slot audit (J4b). */
63
+ onSuccess(_context: ContextType, _token: IToken): Promise<void>;
64
+ /** Slot audit (J4b) — le 401 + challenge sont posés par le firewall. */
65
+ onFailure(_context: ContextType, _error: Error): Promise<void>;
66
+ /** Challenge RFC 6750/7235 posé par le firewall sur les 401 de la zone. */
67
+ challenge(): string;
68
+ }
69
+ export default JwtAuthenticator;
@@ -0,0 +1,70 @@
1
+ import type { ContextType } from "@nodefony/http";
2
+ import type { IUserProvider } from "@nodefony/user";
3
+ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
+ import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
5
+ import type { IToken } from "../../contracts/IToken.js";
6
+ /**
7
+ * Authentification par **session serveur** (cookie opaque, modèle BFF) — la
8
+ * preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
9
+ * l'identifiant dans le blob et régénéré l'ID anti-fixation).
10
+ *
11
+ * `supports()` exige une session REPRISE porteuse d'un utilisateur : le
12
+ * pipeline http démarre la session AVANT le firewall (point d'activation
13
+ * unique, lazy — cookie entrant ou intent de route), cet authenticator ne
14
+ * démarre jamais rien lui-même. L'identité est re-résolue à CHAQUE requête
15
+ * via {@link resolveSessionIdentity} (rôles frais, révocation immédiate).
16
+ *
17
+ * Pas de `challenge()` : une session absente/expirée donne un 401 nu — le
18
+ * client web redirige vers son écran de login, jamais de popup Basic. Si la
19
+ * zone liste aussi `userpassword`, le firewall pose SON challenge (RFC 7235).
20
+ */
21
+ export declare class SessionAuthenticator implements IAuthenticator {
22
+ #private;
23
+ readonly name = "session";
24
+ /**
25
+ * @param resolveProvider - résolution lazy de la source d'identité
26
+ * (typiquement `container.get("users")`) — appelée à la première requête.
27
+ */
28
+ constructor(resolveProvider: () => IUserProvider);
29
+ /**
30
+ * Refuse une zone déclarée SANS REGISTRE — au boot, pas à la première requête.
31
+ *
32
+ * `stateless: true` annonce que l'identité tient tout entière dans la preuve
33
+ * portée par chaque requête, et que la session est ignorée « même si un
34
+ * cookie est présent ». Lister `session` dans une telle zone dit exactement
35
+ * l'inverse : {@link supports} y rendrait vrai dès qu'un cookie ramène une
36
+ * session porteuse d'un utilisateur, et la zone authentifierait par le
37
+ * registre qu'elle déclare ne pas tenir.
38
+ *
39
+ * Cette contradiction ne se voyait NULLE PART : l'application démarrait, la
40
+ * console d'administration affichait « aucun registre serveur », et le
41
+ * cookie authentifiait quand même. Elle se refuse donc au démarrage — le
42
+ * firewall en fait une erreur de configuration fail-closed, plutôt qu'une
43
+ * requête sur deux qui se comporte autrement que ce qui est écrit.
44
+ *
45
+ * @param area - la zone qui liste cet authenticator.
46
+ * @throws Error si la zone est `stateless` — le message la NOMME.
47
+ */
48
+ validateArea(area: ISecuredArea): void;
49
+ /** La requête porte-t-elle une session reprise avec un utilisateur ? */
50
+ supports(context: ContextType): boolean;
51
+ /** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
52
+ createToken(context: ContextType): Promise<IToken>;
53
+ /**
54
+ * Re-résout l'identifiant de session en utilisateur vivant et promeut le
55
+ * token. Les contrôles d'état (existe, actif, non verrouillé) vivent dans
56
+ * {@link resolveSessionIdentity} — partagés avec `AuthFlow.me()`.
57
+ *
58
+ * @throws AuthenticationError (401, message uniforme) — session orpheline,
59
+ * compte verrouillé ou désactivé.
60
+ */
61
+ authenticate(token: IToken): Promise<IToken>;
62
+ /**
63
+ * Pose l'identifiant sur le contexte : la persistance de session du pipeline
64
+ * (`saveSession`) lie le blob au principal courant (string attendu).
65
+ */
66
+ onSuccess(context: ContextType, token: IToken): Promise<void>;
67
+ /** Slot audit (P6.14). Le 401 est posé par le firewall. */
68
+ onFailure(_context: ContextType, _error: Error): Promise<void>;
69
+ }
70
+ export default SessionAuthenticator;
@@ -0,0 +1,53 @@
1
+ import type { ContextType } from "@nodefony/http";
2
+ import type { IPasswordVerifier } from "@nodefony/user";
3
+ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
+ import type { IToken } from "../../contracts/IToken.js";
5
+ import type { LoginThrottler } from "../throttle/LoginThrottler.js";
6
+ /**
7
+ * Authentification par identifiant + mot de passe — schéma **HTTP Basic**
8
+ * (RFC 7617) : `Authorization: Basic base64(identifiant:motdepasse)`, charset
9
+ * UTF-8, split au PREMIER `:` (le mot de passe peut en contenir).
10
+ *
11
+ * La vérification est déléguée au {@link IPasswordVerifier} (`UserService` par
12
+ * défaut) : hash, comparaison, leurre anti-timing et re-hash transparent restent
13
+ * derrière la frontière user — cet authenticator ne voit que le verdict.
14
+ *
15
+ * Le verifier est résolu **paresseusement** au premier login (cold path) : le
16
+ * boot ne paie rien et l'ordre de chargement des modules est indifférent.
17
+ *
18
+ * @remarks Le login par formulaire (body JSON) n'est PAS ici : il arrive avec la
19
+ * session BFF (`AuthController`, J3) qui appelle le verifier directement.
20
+ */
21
+ export declare class UserPasswordAuthenticator implements IAuthenticator {
22
+ #private;
23
+ readonly name = "userpassword";
24
+ /**
25
+ * @param resolveVerifier - résolution lazy de la source de vérification
26
+ * (typiquement `container.get("users")`) — appelée au premier login.
27
+ * @param throttler - limiteur de tentatives (backoff NIST), `null` = désactivé.
28
+ */
29
+ constructor(resolveVerifier: () => IPasswordVerifier, throttler?: LoginThrottler | null);
30
+ /** La requête porte-t-elle un en-tête `Authorization: Basic ...` ? */
31
+ supports(context: ContextType): boolean;
32
+ /** Décode l'enveloppe Basic — un contenu malformé donne un credential vide (échec uniforme). */
33
+ createToken(context: ContextType): Promise<IToken>;
34
+ /**
35
+ * Vérifie le credential via le verifier ou lève un 401 au message uniforme.
36
+ * Au succès le token est promu : utilisateur posé, credential effacé.
37
+ *
38
+ * Throttling NIST (si activé) : l'identifiant SAISI est vérifié AVANT le
39
+ * verifier (un identifiant bloqué ne coûte aucun hash → le throttle protège
40
+ * aussi le serveur du DoS argon2), échec compté, succès remis à zéro.
41
+ *
42
+ * @throws ThrottledError (429 + `Retry-After`) — backoff encore actif.
43
+ * @throws AuthenticationError (401) — credential absent ou invalide.
44
+ */
45
+ authenticate(token: IToken): Promise<IToken>;
46
+ /** Slot J3 (session BFF au login) — rien à poser pour du Basic pur. */
47
+ onSuccess(_context: ContextType, _token: IToken): Promise<void>;
48
+ /** Slot J3+ (audit events). Le throttling vit dans `authenticate` (clé = identifiant) ; le 401 + challenge sont posés par le firewall. */
49
+ onFailure(_context: ContextType, _error: Error): Promise<void>;
50
+ /** Challenge RFC 7235 posé par le firewall sur les 401 de la zone. */
51
+ challenge(): string;
52
+ }
53
+ export default UserPasswordAuthenticator;
@@ -0,0 +1,39 @@
1
+ import type { Container } from "nodefony";
2
+ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
3
+ import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
4
+ /**
5
+ * Registre de **fabriques d'authenticators** — résout les noms listés dans
6
+ * `areas.<zone>.authenticators` vers des instances, SANS que le firewall
7
+ * connaisse le moindre nom en dur.
8
+ *
9
+ * Pourquoi : `IAuthenticator` est pluggable par contrat ; un
10
+ * `if (name === "jwt") …` dans le firewall trahirait cette promesse (couplage
11
+ * aux noms, fermé à l'extension). Les builtins s'enregistrent au chargement du
12
+ * module (donc toujours AVANT le boot) ; un plugin externe enregistre le sien
13
+ * (`registerAuthenticatorFactory("ldap", …)`) puis le référence en config —
14
+ * aucun changement dans le cœur. Convention-frère : `backplaneRegistry`
15
+ * (realtime), `ormRegistry` (orm-core).
16
+ */
17
+ /**
18
+ * Contexte passé à une fabrique : tout ce dont un authenticator peut avoir
19
+ * besoin pour se construire. La fabrique ne fait QUE construire — résolutions
20
+ * de services coûteuses en lazy à l'intérieur de l'instance (cold path).
21
+ */
22
+ export interface IAuthenticatorFactoryContext {
23
+ /** Container DI — résolution de services (`users`, `sessions`...). */
24
+ readonly container: Container;
25
+ /** Config sécurité validée + gelée (sections `jwt`, `passkeys`...). */
26
+ readonly config: ISecurityConfig;
27
+ }
28
+ /** Fabrique d'un authenticator pour un nom donné. */
29
+ export type AuthenticatorFactory = (ctx: IAuthenticatorFactoryContext) => IAuthenticator;
30
+ /**
31
+ * Enregistre (ou remplace) la fabrique d'un authenticator. Appelé par les
32
+ * builtins au chargement du module, et par les plugins pour les leurs
33
+ * (LDAP, SSO maison...).
34
+ */
35
+ export declare function registerAuthenticatorFactory(name: string, factory: AuthenticatorFactory): void;
36
+ /** Fabrique d'un authenticator par nom, ou `undefined` si inconnu. */
37
+ export declare function getAuthenticatorFactory(name: string): AuthenticatorFactory | undefined;
38
+ /** Noms enregistrés (validation boot, introspection Studio, tests). */
39
+ export declare function listAuthenticatorFactories(): string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Lecture d'un en-tête `Authorization: Bearer …`.
3
+ *
4
+ * 🔴 **L'implémentation a déménagé au CŒUR** (`nodefony`), et ce fichier n'en
5
+ * garde que le point d'entrée. Le motif n'est pas cosmétique : deux couches qui
6
+ * ne se voient pas lisent le même en-tête — les authentificateurs d'ici, et le
7
+ * rôle *serveur de ressource* OAuth, qui vit au cœur parce qu'il ne dépend
8
+ * d'aucun module. Une frontière de paquets aurait imposé une copie, et une copie
9
+ * de cette fonction ne diverge pas bruyamment : elle diverge sur un cas limite
10
+ * (`Bearer` sans séparateur, espace insécable, jeton vide) que **chaque copie
11
+ * continue de passer dans ses propres tests**.
12
+ *
13
+ * Ce qui l'a motivée reste vrai et se relit au cœur : le motif d'origine était
14
+ * quadratique, et il s'exécutait avant toute authentification — donc pour un
15
+ * porteur qui n'avait rien prouvé.
16
+ *
17
+ * Les tests d'ici (`tests/unit/bearer.test.ts`, cas anti-ReDoS compris) valent
18
+ * désormais pour l'implémentation du cœur : ils l'atteignent par ce point
19
+ * d'entrée, ce qui est exactement le contrôle qu'on veut sur une brique
20
+ * partagée.
21
+ */
22
+ export { bearerToken } from "nodefony";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Comment le sujet d'un émetteur donné entre dans l'espace de noms local.
3
+ *
4
+ * - `prefixed` — l'identifiant local est composé de l'émetteur ET du sujet.
5
+ * - `subject` — le sujet est pris tel quel (l'espace de noms est maîtrisé).
6
+ */
7
+ export type ExternalSubjectMapping = "prefixed" | "subject";
8
+ /**
9
+ * Compose l'identifiant local qui désigne le sujet d'un émetteur externe.
10
+ *
11
+ * 🔴 **Un `sub` seul ne désigne personne.** OpenID Connect Core §2 ne garantit
12
+ * son unicité et sa non-réattribution que *dans l'espace de son émetteur*.
13
+ * Chercher un compte local directement par `sub` verse donc des identifiants
14
+ * étrangers dans l'espace local : il suffit d'un annuaire où l'utilisateur
15
+ * choisit son identifiant — beaucoup le permettent — pour présenter
16
+ * `sub: "admin"` et se voir rattacher au compte local du même nom.
17
+ *
18
+ * C'est pour cela que `prefixed` est le défaut et que `subject` se déclare :
19
+ * le mode sûr ne doit rien demander, le mode qui fait confiance doit être écrit.
20
+ *
21
+ * @param issuer - émetteur VÉRIFIÉ, sous sa forme canonique (jamais la valeur
22
+ * brute lue dans le jeton — elle est choisie par le porteur)
23
+ * @param subject - sujet du jeton (`sub`)
24
+ * @param mapping - politique déclarée pour CET émetteur
25
+ * @returns l'identifiant à chercher dans l'annuaire local
26
+ */
27
+ export declare function localIdentifierFor(issuer: string, subject: string, mapping: ExternalSubjectMapping): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Lecture NON VÉRIFIÉE de l'émetteur d'un JWS compact.
3
+ *
4
+ * Sert à **choisir qui doit examiner un jeton**, jamais à décider de son sort.
5
+ * Deux authenticators reconnaissent la même forme de credential — un
6
+ * `Authorization: Bearer <jws>` — l'un pour les jetons que Nodefony a émis,
7
+ * l'autre pour ceux d'un serveur d'autorisation tiers. Sans discriminant, le
8
+ * premier listé dans la zone capture les deux et refuse la moitié des jetons :
9
+ * l'ordre de la configuration deviendrait une décision de sécurité, et son
10
+ * erreur ne se verrait qu'en production.
11
+ *
12
+ * ## Ce qui rend cette lecture sûre
13
+ *
14
+ * Rien de ce qui est lu ici ne devient une clé, une URL ou un algorithme. La
15
+ * valeur ne sert qu'à sélectionner une entrée dans une liste **fermée**, écrite
16
+ * en configuration ; le jeton est ensuite vérifié entièrement par
17
+ * l'authenticator retenu, `iss` compris. Un attaquant qui ment sur `iss` ne
18
+ * gagne donc que le droit d'être refusé par un autre maillon.
19
+ *
20
+ * La taille est bornée AVANT tout travail : `JSON.parse` sur une entrée non
21
+ * fiable est le genre d'appel qu'on ne laisse pas grandir sans limite.
22
+ */
23
+ /**
24
+ * Rend le claim `iss` d'un JWS compact, sans vérifier quoi que ce soit.
25
+ *
26
+ * @param raw - le jeton brut, tel que présenté
27
+ * @returns l'émetteur revendiqué, ou `null` si le jeton est trop gros, mal
28
+ * formé, ou ne revendique pas d'émetteur exploitable
29
+ */
30
+ export declare function peekIssuer(raw: string): string | null;
31
+ export default peekIssuer;
@@ -0,0 +1,31 @@
1
+ import { Buffer } from "node:buffer";
2
+ /** Contexte de dérivation HKDF — distingue les domaines cryptographiques. */
3
+ export interface IKeyDerivation {
4
+ /** Sel HKDF (RFC 5869 §3.1) — constante par domaine. */
5
+ readonly salt: string | Buffer;
6
+ /** Info HKDF (RFC 5869 §3.2) — lie la sous-clé à son usage (séparation de domaine). */
7
+ readonly info: string | Buffer;
8
+ }
9
+ /**
10
+ * Dérive une clé AES-256 (32 octets) d'un matériel de clé applicatif via
11
+ * HKDF-SHA256 (RFC 5869). Accepte toute longueur/forme (passphrase, hex, base64)
12
+ * sans jamais l'utiliser brute comme clé AES. **Déterministe** : tous les pods
13
+ * d'un cluster dérivent la même clé du même secret de config + même domaine
14
+ * (secret lisible cross-pod). Domaines distincts (`info` différent) → clés
15
+ * indépendantes.
16
+ *
17
+ * @param material - matériel de clé brut (secret de config).
18
+ * @param derivation - sel + info propres au domaine (TOTP, webhook…).
19
+ * @returns clé AES-256 (32 octets).
20
+ */
21
+ export declare function deriveKey(material: string | Buffer, derivation: IKeyDerivation): Buffer;
22
+ /** Génère une clé éphémère 32 octets (dev sans clé configurée — non persistée). */
23
+ export declare function generateEphemeralKey(): Buffer;
24
+ /** Chiffre un secret en clair → blob opaque versionné (IV aléatoire à chaque appel). */
25
+ export declare function encryptSecret(plain: Buffer, key: Buffer): string;
26
+ /**
27
+ * Déchiffre un blob produit par {@link encryptSecret}. Lève si le format/version
28
+ * est invalide, le blob tronqué, ou le tag GCM non valide (altération OU mauvaise
29
+ * clé — GCM ne distingue pas les deux, par construction).
30
+ */
31
+ export declare function decryptSecret(blob: string, key: Buffer): Buffer;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Fusion de directives Content-Security-Policy — logique PURE (aucune dépendance
3
+ * à Vite, au frontend ni au transport). Permet à un module de DÉCLARER ses besoins
4
+ * CSP (ex. `@nodefony/frontend` en dev : origines Vite + `'unsafe-eval'` pour le
5
+ * Fast Refresh) sans que `@nodefony/security` connaisse leur sémantique : security
6
+ * se contente de MERGER des fragments génériques `directive → sources`.
7
+ *
8
+ * Pourquoi un merge structuré (et pas une simple concaténation) : en CSP une
9
+ * directive RÉPÉTÉE est ignorée après sa 1ʳᵉ occurrence (W3C CSP3 §3) → concaténer
10
+ * deux `script-src` perdrait le second. Il faut fusionner les sources dans UNE
11
+ * directive. Le token `'nonce-{{nonce}}'` est opaque (ni `;` ni espace) → préservé
12
+ * tel quel par parse/serialize.
13
+ */
14
+ /** Fragment additif d'un module : directive CSP → sources à ajouter. */
15
+ export type CspFragment = Record<string, readonly string[]>;
16
+ /**
17
+ * Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
18
+ * directives et des sources est préservé (déterminisme : header stable, tests
19
+ * fiables). Les séparateurs multiples / espaces superflus sont normalisés.
20
+ */
21
+ export declare function parseCsp(csp: string): Array<[string, string[]]>;
22
+ /** Sérialise des directives ordonnées en chaîne CSP (`a b; c d`). */
23
+ export declare function serializeCsp(directives: Array<[string, string[]]>): string;
24
+ /**
25
+ * Fusionne un CSP de base avec des fragments additifs par module.
26
+ *
27
+ * - directive déjà dans la base → ses sources sont COMPLÉTÉES (dédupliquées,
28
+ * ordre base d'abord puis ajouts) ;
29
+ * - directive absente → AJOUTÉE en fin (ordre d'apparition des fragments).
30
+ *
31
+ * Pur + déterministe : recalculé uniquement quand un module (dé)enregistre ses
32
+ * origines (jamais par requête). Le résultat repart dans `SecurityHeaders`, qui
33
+ * re-split autour de `{{nonce}}` au boot → 1 `join` par requête (hot-path inchangé).
34
+ *
35
+ * @param base - CSP de configuration (peut contenir `'nonce-{{nonce}}'`).
36
+ * @param fragments - fragments additifs (un par module enregistré).
37
+ * @returns la chaîne CSP fusionnée.
38
+ */
39
+ export declare function mergeCspFragments(base: string, fragments: Iterable<CspFragment>): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Jeton synchronizer CSRF — modèle **double-submit signé** (OWASP CSRF Prevention
3
+ * Cheat Sheet, « Signed Double-Submit Cookie »). Le token est
4
+ * `nonce.HMAC-SHA256(secret, nonce)` (base64url), posé dans un cookie LISIBLE
5
+ * (`csrf-token`, non HttpOnly) ET rejoué par le client dans l'en-tête
6
+ * `x-csrf-token`. La défense `@CsrfProtect` exige les deux PRÉSENTS, ÉGAUX
7
+ * (double-submit) et la signature HMAC VALIDE.
8
+ *
9
+ * **Stateless** (aucune session requise) → couvre le BFF (cookie de session) ET
10
+ * l'API JWT sans coupler au stockage de session. Le secret HMAC empêche un script
11
+ * tiers de forger un token (il ne peut pas calculer la signature) ; le double
12
+ * submit empêche un attaquant cross-site d'en injecter un (il ne peut ni écrire
13
+ * l'en-tête custom — préflight CORS — ni lire le cookie de la victime — SameSite + SOP).
14
+ *
15
+ * Pure et synchrone (1 HMAC à l'émission, 1 à la vérif) — payé UNIQUEMENT sur les
16
+ * routes `@CsrfProtect` (la défense globale Fetch Metadata reste primaire, hot-path
17
+ * GET = 0).
18
+ *
19
+ * @see OWASP CSRF Prevention Cheat Sheet · RFC 9110 §15.5.4 (403).
20
+ */
21
+ export declare class CsrfTokenManager {
22
+ #private;
23
+ constructor(secret: string);
24
+ /** Émet un token signé `nonce.signature` (base64url). */
25
+ issue(): string;
26
+ /**
27
+ * Vérifie une mutation `@CsrfProtect` : en-tête ET cookie présents, ÉGAUX
28
+ * (double-submit, comparaison à temps constant) et signature HMAC valide. Tout
29
+ * écart → `false` (le firewall lève alors un 403). Jamais d'exception.
30
+ *
31
+ * @param headerToken - valeur de l'en-tête `x-csrf-token` rejouée par le client.
32
+ * @param cookieToken - valeur du cookie `csrf-token` posé par le serveur.
33
+ */
34
+ verify(headerToken: string | undefined, cookieToken: string | undefined): boolean;
35
+ }
36
+ export default CsrfTokenManager;