@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,39 @@
1
+ import { nodefonyError } from "nodefony";
2
+ //#region nodefony/errors/InvalidTargetError.ts
3
+ /**
4
+ * La ressource demandée à l'émission ne peut pas être servie — `code = 400`,
5
+ * code d'erreur OAuth `invalid_target` (RFC 8707 §2).
6
+ *
7
+ * Le paramètre `resource` dit POUR QUI le jeton est demandé. Trois raisons de
8
+ * refuser, et la RFC les couvre d'un seul code : « The requested resource is
9
+ * invalid, missing, unknown, or malformed. »
10
+ *
11
+ * **Pourquoi refuser plutôt qu'ignorer.** Un `resource` accepté puis jeté rend
12
+ * un jeton parfaitement valide… pour quelqu'un d'autre. Le client croit tenir
13
+ * une clé pour la porte A, la présente, reçoit un `401`, et n'a aucun moyen de
14
+ * comprendre que sa demande n'a jamais été honorée : l'erreur se manifeste chez
15
+ * la ressource, loin de l'endroit où elle a été commise. Refuser à l'émission
16
+ * met le diagnostic là où la faute est.
17
+ *
18
+ * **Le message est constant** et ne nomme pas les audiences acceptées : les
19
+ * énumérer offrirait la carte des ressources protégées de l'application à qui
20
+ * possède un simple identifiant. La valeur refusée, elle, vient du client — la
21
+ * lui rendre ne lui apprend rien.
22
+ */
23
+ var InvalidTargetError = class extends nodefonyError {
24
+ /** Code d'erreur OAuth à rendre au client (RFC 6749 §5.2 / RFC 8707 §2). */
25
+ oauthError = "invalid_target";
26
+ /** Ce que le client a demandé, tel qu'il l'a écrit — pour le journal. */
27
+ requested;
28
+ /**
29
+ * @param description - raison, destinée à `error_description` (aucune fuite :
30
+ * elle qualifie la DEMANDE, jamais la configuration du serveur)
31
+ * @param requested - la valeur refusée, pour le journal
32
+ */
33
+ constructor(description, requested) {
34
+ super(description, 400);
35
+ this.requested = requested;
36
+ }
37
+ };
38
+ //#endregion
39
+ export { InvalidTargetError, InvalidTargetError as default };
@@ -0,0 +1,17 @@
1
+ import { nodefonyError } from "nodefony";
2
+ //#region nodefony/errors/SsrfError.ts
3
+ /**
4
+ * URL rejetée par la protection SSRF — `code = 422`. Levée quand une URL sortante
5
+ * (endpoint webhook, fetch applicatif…) est syntaxiquement valide mais cible une
6
+ * ressource **interdite** : protocole non autorisé, identifiants embarqués, hôte
7
+ * non résolvable, ou IP non publique (loopback, privée, link-local, métadonnées
8
+ * cloud `169.254.169.254`…). Sémantique alignée sur GitHub (422 à l'enregistrement
9
+ * d'un webhook invalide).
10
+ */
11
+ var SsrfError = class extends nodefonyError {
12
+ constructor(message = "URL sortante interdite (SSRF)") {
13
+ super(message, 422);
14
+ }
15
+ };
16
+ //#endregion
17
+ export { SsrfError, SsrfError as default };
@@ -0,0 +1,21 @@
1
+ import { nodefonyError } from "nodefony";
2
+ //#region nodefony/errors/ThrottledError.ts
3
+ /**
4
+ * Tentatives de login trop rapprochées — `code = 429` (RFC 6585 §4).
5
+ *
6
+ * Levée par `UserPasswordAuthenticator` quand le backoff progressif
7
+ * ({@link LoginThrottler}) bloque encore l'identifiant saisi. Distincte du 401 :
8
+ * un client légitime doit savoir QU'ATTENDRE (header `Retry-After`, posé par le
9
+ * firewall), pas re-soumettre en boucle. Le message reste générique — la
10
+ * politique de throttle (seuils, compteurs) n'est jamais détaillée au client.
11
+ */
12
+ var ThrottledError = class extends nodefonyError {
13
+ /** Secondes restantes avant la prochaine tentative autorisée (header `Retry-After`). */
14
+ retryAfterS;
15
+ constructor(retryAfterS) {
16
+ super("Too many attempts", 429);
17
+ this.retryAfterS = retryAfterS;
18
+ }
19
+ };
20
+ //#endregion
21
+ export { ThrottledError, ThrottledError as default };
@@ -0,0 +1,42 @@
1
+ import { nodefonyError } from "nodefony";
2
+ //#region nodefony/errors/UnverifiableTokenError.ts
3
+ /**
4
+ * Le jeton n'a pas pu être VÉRIFIÉ — `code = 503`, et surtout **pas 401**.
5
+ *
6
+ * Levée quand ce qui sait valider un jeton est absent ou en panne : aucun
7
+ * vérificateur posé au conteneur, émetteur injoignable, jeu de clés
8
+ * inutilisable. Le jeton n'est alors ni valide ni invalide — on n'en sait
9
+ * rien, et c'est une information différente.
10
+ *
11
+ * **Pourquoi une erreur distincte.** Répondre 401 à une panne envoie le client
12
+ * chercher un autre jeton, qui échouera pareil : la boucle de renouvellement
13
+ * remplace la panne par une tempête de requêtes, pendant que le tableau de bord
14
+ * affiche une hausse d'« échecs d'authentification » qui ne désigne aucun
15
+ * coupable. Le 503 dit la vérité — le service ne peut pas répondre — et le
16
+ * client légitime attend au lieu d'insister.
17
+ *
18
+ * C'est la même distinction que celle tenue par le vérificateur lui-même, où
19
+ * un refus rend `null` et une panne lève : la porte doit la conserver jusqu'à
20
+ * la réponse, sinon elle est perdue là où elle sert.
21
+ *
22
+ * Le message est **constant**, et c'est structurel : il est rendu au client. La
23
+ * cause technique — nom de l'émetteur défaillant, URL du jeu de clés, erreur
24
+ * réseau — vit dans {@link detail}, que seul le journal lit. Composer la cause
25
+ * dans le message revient à publier la topologie interne de l'authentification
26
+ * à qui présente un jeton quelconque, et le rendu d'erreur de développement y
27
+ * ajoute la pile d'appels par-dessus.
28
+ */
29
+ var UnverifiableTokenError = class extends nodefonyError {
30
+ /** Cause technique, destinée au JOURNAL — jamais au client. */
31
+ detail;
32
+ /**
33
+ * @param detail - cause technique pour le journal ; n'apparaît jamais dans le
34
+ * message rendu au client
35
+ */
36
+ constructor(detail) {
37
+ super("Token verification unavailable", 503);
38
+ this.detail = detail;
39
+ }
40
+ };
41
+ //#endregion
42
+ export { UnverifiableTokenError, UnverifiableTokenError as default };
@@ -0,0 +1,21 @@
1
+ import { nodefonyError } from "nodefony";
2
+ //#region nodefony/errors/WebAuthnError.ts
3
+ /**
4
+ * Erreur de **gestion** d'un credential WebAuthn (enrôlement) — porte un `code`
5
+ * HTTP que l'adaptateur framework mappe par duck-typing (il n'importe jamais les
6
+ * classes de `@nodefony/security`).
7
+ *
8
+ * - `409` — plafond `passkeys.maxPerUser` atteint.
9
+ *
10
+ * Distincte de la **cérémonie** elle-même (défi/signature/origine invalides →
11
+ * `AuthenticationError` 401, message uniforme anti-énumération) : ici la
12
+ * cérémonie a réussi cryptographiquement, c'est la politique du serveur qui
13
+ * refuse d'enregistrer un credential de plus.
14
+ */
15
+ var WebAuthnError = class extends nodefonyError {
16
+ constructor(message, code) {
17
+ super(message, code);
18
+ }
19
+ };
20
+ //#endregion
21
+ export { WebAuthnError, WebAuthnError as default };
@@ -0,0 +1,9 @@
1
+ import { CsrfError } from "./CsrfError.js";
2
+ import { AuthenticationError } from "./AuthenticationError.js";
3
+ import { ThrottledError } from "./ThrottledError.js";
4
+ import { UnverifiableTokenError } from "./UnverifiableTokenError.js";
5
+ import { InvalidTargetError } from "./InvalidTargetError.js";
6
+ import { WebAuthnError } from "./WebAuthnError.js";
7
+ import { SsrfError } from "./SsrfError.js";
8
+ import { AccessDeniedError } from "./AccessDeniedError.js";
9
+ export { AccessDeniedError, AuthenticationError, CsrfError, InvalidTargetError, SsrfError, ThrottledError, UnverifiableTokenError, WebAuthnError };
@@ -0,0 +1,77 @@
1
+ import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
+ import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
3
+ import { ACCESS_TOKEN_VERIFIER, Service, canonicalIssuer } from "nodefony";
4
+ //#region nodefony/service/accessTokenVerifier.ts
5
+ const serviceName = ACCESS_TOKEN_VERIFIER;
6
+ /**
7
+ * Pose (ou non) le vérificateur de jetons d'accès TIERS dans le conteneur.
8
+ *
9
+ * Ce service est une **décision de câblage**, pas de la cryptographie : toute la
10
+ * mécanique vit dans {@link RemoteJwtVerifier}, et la doctrine de refus dans le
11
+ * cœur (`nodefony/src/oauth/`). Ici, on lit la configuration et on tranche une
12
+ * seule question — cette application accepte-t-elle des jetons émis ailleurs ?
13
+ *
14
+ * **Aucun émetteur déclaré = rien n'est posé**, et c'est le comportement voulu :
15
+ * une porte protégée qui ne trouve pas de vérificateur refuse de servir en le
16
+ * disant (503 + CRITIC), là où un vérificateur présent mais vide refuserait
17
+ * chaque jeton un par un — même résultat pour l'appelant, diagnostic beaucoup
18
+ * plus difficile pour l'exploitant.
19
+ *
20
+ * Le coût est nul quand la capacité n'est pas utilisée : rien n'est instancié,
21
+ * aucune requête n'est faite au démarrage. La découverte des clés n'a lieu qu'au
22
+ * PREMIER jeton réellement présenté, et une seule fois par émetteur.
23
+ */
24
+ var AccessTokenVerifierService = class extends Service {
25
+ module;
26
+ #verifier = null;
27
+ constructor(module) {
28
+ super(serviceName, module.container, module.notificationsCenter, module.options);
29
+ this.module = module;
30
+ this.kernel?.once("onBoot", () => this.#build());
31
+ }
32
+ /** Le vérificateur, ou `null` si aucun émetteur n'est déclaré. */
33
+ get verifier() {
34
+ return this.#verifier;
35
+ }
36
+ #build() {
37
+ let config;
38
+ try {
39
+ config = defineSecurityConfig(this.options);
40
+ } catch {
41
+ return;
42
+ }
43
+ const { issuers, ...tuning } = config.resourceServer;
44
+ if (issuers.length === 0) {
45
+ this.log("aucun émetteur de confiance déclaré (security.resourceServer.issuers) — les jetons émis par un serveur d'autorisation tiers ne sont pas vérifiables ; une porte protégée refusera de servir.", "DEBUG");
46
+ return;
47
+ }
48
+ const localIssuer = config.jwt.issuer;
49
+ const armed = issuers.map((trusted) => localIssuer && canonicalIssuer(trusted.issuer) === canonicalIssuer(localIssuer) ? {
50
+ ...trusted,
51
+ localJwks: async () => {
52
+ const keystore = this.container?.get("jwtKeystore");
53
+ if (!keystore) throw new Error(`l'émetteur « ${trusted.issuer} » est cette application, mais sa capacité JWT n'est pas armée (security.jwt) — aucune clé locale à présenter`);
54
+ return keystore.getPublicJWKS();
55
+ }
56
+ } : trusted);
57
+ try {
58
+ this.#verifier = new RemoteJwtVerifier({
59
+ issuers: armed,
60
+ timeoutMs: tuning.timeoutMs,
61
+ cooldownMs: tuning.cooldownMs,
62
+ cacheMaxAgeMs: tuning.cacheMaxAgeMs,
63
+ clockToleranceS: tuning.clockToleranceS,
64
+ log: (message) => this.log(message, "DEBUG")
65
+ });
66
+ } catch (error) {
67
+ this.log(`vérificateur de jetons non armé — ${error.message}`, "CRITIC");
68
+ return;
69
+ }
70
+ const verify = (token, audience) => this.#verifier.verify(token, audience);
71
+ this.container?.set(serviceName, verify);
72
+ const locaux = armed.filter((i) => "localJwks" in i).length;
73
+ this.log(`vérificateur de jetons armé — ${issuers.length} émetteur(s) de confiance : ` + issuers.map((i) => i.issuer).join(", ") + (locaux > 0 ? ` (dont ${locaux} servi par les clés LOCALES, sans requête)` : ""), "INFO");
74
+ }
75
+ };
76
+ //#endregion
77
+ export { AccessTokenVerifierService, AccessTokenVerifierService as default };
@@ -0,0 +1,310 @@
1
+ import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
+ import { generateApiKey } from "../src/apikey/apiKeyFormat.js";
3
+ import { recordAudit } from "../src/audit/recordAudit.js";
4
+ import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
5
+ import { ApiKeyError } from "../errors/ApiKeyError.js";
6
+ import { Service, countFacets } from "nodefony";
7
+ import { randomUUID } from "node:crypto";
8
+ //#region nodefony/service/apiKeys.ts
9
+ const serviceName = "apiKeys";
10
+ const MAX_NAME_LEN = 100;
11
+ const MS_PER_DAY = 864e5;
12
+ /**
13
+ * Gestion des **clés API personnelles (PAT, P6.12)** — émission, listing et
14
+ * révocation, au-dessus du `ITokenStore` **partagé** (posé au container par le
15
+ * `TokenService`, qui en possède aussi le `gc`). Un PAT et un refresh token
16
+ * cohabitent dans la même table (`kind`) ; ce service ne traite que `kind:"pat"`.
17
+ *
18
+ * **Sécurité** : le secret (256 bits aléatoires) n'est rendu en clair qu'à la
19
+ * création (`IApiKeyCreated.token`, RFC « shown once ») ; seul son `sha256` est
20
+ * persisté. Création/révocation s'appliquent **toujours à un porteur donné**
21
+ * (jamais à autrui) — l'identité est résolue côté endpoint (session BFF). La
22
+ * vérification d'une clé présentée vit, elle, dans `ApiKeyAuthenticator`.
23
+ *
24
+ * Le store est résolu **paresseusement** du container (`tokenStore`) au premier
25
+ * usage : indépendant de l'ordre de boot des services.
26
+ */
27
+ var ApiKeyService = class extends Service {
28
+ module;
29
+ #store = null;
30
+ #enabled = false;
31
+ #prefix = "nf";
32
+ #defaultExpiryDays = 90;
33
+ #maxPerSubject = 100;
34
+ #allowedScopes = null;
35
+ constructor(module) {
36
+ super(serviceName, module.container, module.notificationsCenter, module.options);
37
+ this.module = module;
38
+ this.kernel?.once("onBoot", () => this.#build());
39
+ }
40
+ #build() {
41
+ let config;
42
+ try {
43
+ config = defineSecurityConfig(this.options);
44
+ } catch {
45
+ return;
46
+ }
47
+ const ak = config.apiKeys;
48
+ if (!ak.enabled) {
49
+ this.log("api keys idle — désactivées en config", "DEBUG");
50
+ return;
51
+ }
52
+ this.#enabled = true;
53
+ this.#prefix = ak.prefix;
54
+ this.#defaultExpiryDays = ak.defaultExpiryDays;
55
+ this.#maxPerSubject = ak.maxPerSubject;
56
+ this.#allowedScopes = ak.allowedScopes;
57
+ this.log(`api keys ready — prefix "${ak.prefix}_", max ${ak.maxPerSubject}/porteur`, "DEBUG");
58
+ }
59
+ /** `true` si les clés API sont activées en config. */
60
+ isEnabled() {
61
+ return this.#enabled;
62
+ }
63
+ /**
64
+ * Champs de tri que le backend **actuellement branché** sait honorer, en
65
+ * vocabulaire public. Le data plane admin les passe en allowlist au traducteur
66
+ * de requête de page : hors de cette liste, un `?order=` est refusé en 400.
67
+ *
68
+ * La liste vient du store, jamais d'une constante recopiée ici : c'est ce qui
69
+ * fait qu'un backend à capacité réduite (Redis, dont le `SCAN` n'a pas d'ordre
70
+ * global) refuse le tri **sans qu'aucune règle supplémentaire ne soit écrite**.
71
+ * Store absent ou indisponible → aucune capacité annoncée, donc aucun tri promis.
72
+ *
73
+ * @returns les champs triables, ou un tableau vide.
74
+ */
75
+ sortableFields() {
76
+ try {
77
+ return this.#resolveStore().sortableFields ?? [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+ /**
83
+ * Émet une nouvelle clé API pour un porteur — renvoie sa vue publique **+ le
84
+ * token en clair** (affiché une seule fois).
85
+ *
86
+ * @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
87
+ * @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
88
+ * @throws ApiKeyError 503 — store indisponible.
89
+ */
90
+ async createForSubject(subjectId, subjectType, opts) {
91
+ const store = this.#resolveStore();
92
+ const name = this.#normalizeName(opts.name);
93
+ const scopes = this.#normalizeScopes(opts.scopes);
94
+ const now = Date.now();
95
+ const expiresAt = this.#resolveExpiry(opts.expiresInDays, now);
96
+ if ((await store.findBySubject(subjectId)).filter((r) => r.kind === "pat" && this.#isActive(r, now)).length >= this.#maxPerSubject) throw new ApiKeyError(`API key limit reached (${this.#maxPerSubject})`, 409);
97
+ const generated = generateApiKey(this.#prefix);
98
+ const record = {
99
+ id: randomUUID(),
100
+ kind: "pat",
101
+ name,
102
+ prefix: generated.publicPrefix,
103
+ subjectId,
104
+ subjectType,
105
+ tenantId: opts.tenantId ?? null,
106
+ scopes,
107
+ audience: [],
108
+ resources: null,
109
+ secretHash: generated.secretHash,
110
+ hashAlg: "sha256",
111
+ clientId: null,
112
+ cnf: null,
113
+ family: null,
114
+ replacedBy: null,
115
+ createdAt: now,
116
+ expiresAt,
117
+ lastUsedAt: null,
118
+ lastUsedIp: null,
119
+ lastUsedUserAgent: null,
120
+ revokedAt: null,
121
+ revokedReason: null,
122
+ metadata: {}
123
+ };
124
+ await store.put(record);
125
+ this.log(`api key created — id=${record.id} subject=${subjectId} scopes=[${scopes.join(",")}]`, "INFO");
126
+ recordAudit(this.container, {
127
+ category: "token",
128
+ action: "apikey.created",
129
+ outcome: "success",
130
+ actor: subjectId,
131
+ resource: record.id,
132
+ metadata: {
133
+ scopes,
134
+ subjectType
135
+ }
136
+ });
137
+ return {
138
+ ...this.#toView(record),
139
+ token: generated.token
140
+ };
141
+ }
142
+ /** Liste les clés (PAT) d'un porteur — vue publique, sans secret, récentes d'abord. */
143
+ async listForSubject(subjectId) {
144
+ return (await this.#resolveStore().findBySubject(subjectId)).filter((r) => r.kind === "pat").map((r) => this.#toView(r)).sort((a, b) => b.createdAt - a.createdAt);
145
+ }
146
+ /**
147
+ * Liste **paginée** des clés (PAT) du système, tous porteurs confondus — vue
148
+ * d'ADMINISTRATION (gouvernance / réponse à incident), publique et sans secret.
149
+ * Réservé au data plane admin (RBAC `ROLE_NODEFONY_ADMIN`) : l'identité du porteur
150
+ * (`subjectId`) est exposée pour la supervision.
151
+ *
152
+ * Pagination **native au store** (jamais un `listAll()` matérialisé en RAM) : `kind`
153
+ * est forcé à `"pat"` ; les autres filtres (`subjectId`/`revoked`) + la fenêtre
154
+ * (`limit`/`offset`/`cursor`) viennent de `query`. Tri `createdAt` DESC par défaut.
155
+ *
156
+ * @param query - filtres + fenêtre de page ({@link ITokenListQuery}, `kind` ignoré).
157
+ * @returns une page de vues publiques ({@link IApiKeyView}, sans secret).
158
+ */
159
+ async listPagePat(query) {
160
+ const page = await this.#resolveStore().listPage({
161
+ ...query,
162
+ kind: "pat"
163
+ });
164
+ return {
165
+ ...page,
166
+ items: page.items.map((r) => this.#toView(r))
167
+ };
168
+ }
169
+ /**
170
+ * Les compteurs de tête de la console — posés sur la collection ENTIÈRE, pas
171
+ * sur la page affichée.
172
+ *
173
+ * Les trois états partitionnent, mais chacun est **compté** : une partition
174
+ * est une propriété du domaine d'aujourd'hui, pas une garantie du code, et un
175
+ * quatrième état la briserait en silence si l'un se déduisait des autres.
176
+ *
177
+ * `kind` reste forcé à `"pat"` comme pour la liste : ces cartes surplombent
178
+ * un tableau de clés d'API, pas de jetons de rafraîchissement.
179
+ *
180
+ * @param query - filtres à appliquer avant comptage (sans fenêtre).
181
+ */
182
+ async countKeyFacets(query) {
183
+ const store = this.#resolveStore();
184
+ return countFacets(TOKEN_FACETS, (facet) => store.countTokens({
185
+ ...query,
186
+ ...facet,
187
+ kind: "pat"
188
+ }));
189
+ }
190
+ /**
191
+ * Révoque **n'importe quelle** clé (PAT) — action d'ADMINISTRATION (réponse à
192
+ * incident : clé compromise), SANS contrainte de porteur (≠ `revokeForSubject`).
193
+ * Audité avec l'acteur admin ET le porteur cible. Idempotent.
194
+ *
195
+ * @param id - identifiant public de la clé.
196
+ * @param actorId - identité de l'admin qui révoque (tracée pour l'audit).
197
+ * @returns la vue publique mise à jour, ou `null` si introuvable / pas un PAT.
198
+ */
199
+ async revokeAnyPat(id, actorId) {
200
+ const store = this.#resolveStore();
201
+ const record = await store.findById(id);
202
+ if (!record || record.kind !== "pat") return null;
203
+ await store.revoke(id, "manual");
204
+ this.log(`api key revoked by admin — id=${id} actor=${actorId} subject=${record.subjectId}`, "INFO");
205
+ recordAudit(this.container, {
206
+ category: "token",
207
+ action: "apikey.revoked",
208
+ outcome: "success",
209
+ actor: actorId,
210
+ resource: id,
211
+ reason: "manual",
212
+ metadata: {
213
+ subject: record.subjectId,
214
+ viaAdmin: true
215
+ }
216
+ });
217
+ const updated = await store.findById(id);
218
+ return updated ? this.#toView(updated) : null;
219
+ }
220
+ /**
221
+ * Capacités/contraintes d'émission (plafond, scopes, préfixe, durée par défaut)
222
+ * — pour un formulaire de création honnête côté console. Aucune valeur sensible.
223
+ */
224
+ describeCapabilities() {
225
+ return {
226
+ enabled: this.#enabled,
227
+ prefix: this.#prefix,
228
+ defaultExpiryDays: this.#defaultExpiryDays,
229
+ maxPerSubject: this.#maxPerSubject,
230
+ allowedScopes: this.#allowedScopes === null ? null : [...this.#allowedScopes]
231
+ };
232
+ }
233
+ /**
234
+ * Révoque une clé du porteur. **Anti-énumération** : une clé inexistante OU
235
+ * appartenant à autrui renvoie `false` (« introuvable pour ce porteur ») —
236
+ * jamais un 403 qui révélerait son existence. Idempotent (déjà révoquée → `true`).
237
+ *
238
+ * @returns `true` si la clé du porteur a été trouvée (et révoquée), sinon `false`.
239
+ */
240
+ async revokeForSubject(subjectId, id) {
241
+ const store = this.#resolveStore();
242
+ const record = await store.findById(id);
243
+ if (!record || record.kind !== "pat" || record.subjectId !== subjectId) return false;
244
+ await store.revoke(id, "manual");
245
+ this.log(`api key revoked — id=${id} subject=${subjectId}`, "INFO");
246
+ recordAudit(this.container, {
247
+ category: "token",
248
+ action: "apikey.revoked",
249
+ outcome: "success",
250
+ actor: subjectId,
251
+ resource: id,
252
+ reason: "manual"
253
+ });
254
+ return true;
255
+ }
256
+ #resolveStore() {
257
+ if (this.#store === null) {
258
+ const store = this.get("tokenStore");
259
+ if (!store) throw new ApiKeyError("API key store unavailable", 503);
260
+ this.#store = store;
261
+ }
262
+ return this.#store;
263
+ }
264
+ #normalizeName(raw) {
265
+ if (typeof raw !== "string" || raw.trim().length === 0) throw new ApiKeyError("API key name is required", 400);
266
+ const name = raw.trim();
267
+ if (name.length > MAX_NAME_LEN) throw new ApiKeyError(`API key name too long (max ${MAX_NAME_LEN})`, 400);
268
+ return name;
269
+ }
270
+ #normalizeScopes(raw) {
271
+ if (raw === void 0 || raw === null) return [];
272
+ if (!Array.isArray(raw)) throw new ApiKeyError("scopes must be an array of strings", 400);
273
+ const scopes = [];
274
+ for (const entry of raw) {
275
+ if (typeof entry !== "string" || entry.trim().length === 0) throw new ApiKeyError("invalid scope", 400);
276
+ const scope = entry.trim();
277
+ if (this.#allowedScopes !== null && !this.#allowedScopes.includes(scope)) throw new ApiKeyError(`scope not allowed: ${scope}`, 400);
278
+ if (!scopes.includes(scope)) scopes.push(scope);
279
+ }
280
+ return scopes;
281
+ }
282
+ #resolveExpiry(expiresInDays, now) {
283
+ const days = expiresInDays === void 0 ? this.#defaultExpiryDays : expiresInDays;
284
+ if (days === null) return null;
285
+ if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) throw new ApiKeyError("expiresInDays must be a positive number or null", 400);
286
+ return now + days * MS_PER_DAY;
287
+ }
288
+ #isActive(record, now) {
289
+ return record.revokedAt === null && (record.expiresAt === null || record.expiresAt > now);
290
+ }
291
+ #toView(record) {
292
+ return {
293
+ id: record.id,
294
+ prefix: record.prefix,
295
+ name: record.name,
296
+ scopes: [...record.scopes],
297
+ subjectId: record.subjectId,
298
+ subjectType: record.subjectType,
299
+ tenantId: record.tenantId,
300
+ createdAt: record.createdAt,
301
+ expiresAt: record.expiresAt,
302
+ lastUsedAt: record.lastUsedAt,
303
+ lastUsedIp: record.lastUsedIp,
304
+ lastUsedUserAgent: record.lastUsedUserAgent,
305
+ revokedAt: record.revokedAt
306
+ };
307
+ }
308
+ };
309
+ //#endregion
310
+ export { ApiKeyService, ApiKeyService as default };