@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,82 @@
1
+ import { PLATFORM_CHANNELS } from "nodefony";
2
+ //#region nodefony/src/audit/auditBridge.ts
3
+ /**
4
+ * Canal WS du flux live d'audit (P6.14 lot 4). Le préfixe `security:` le place
5
+ * sous le plancher `SECURITY_CHANNEL_POLICY` (ROLE_NODEFONY_ADMIN) du verrou de
6
+ * frame — un user lambda ne peut pas s'y abonner (refus audité `frame.denied`).
7
+ */
8
+ const SECURITY_AUDIT_CHANNEL = PLATFORM_CHANNELS.audit;
9
+ /**
10
+ * Pont journal d'audit → canal `nodefony:audit`, **coalescé** (P6.14 lot 4).
11
+ *
12
+ * Calque {@link createSyslogBridge} (studio) : au lieu de 1 frame WS par
13
+ * événement (un pic d'`auth.failure` sous brute-force noierait la console
14
+ * auditeur), on accumule dans un **ring buffer borné** et on flush **1 frame
15
+ * agrégée toutes les `flushMs`** : `{ events, dropped }`. Sous surcharge, le ring
16
+ * écrase les plus vieux et `dropped` indique combien ont été omis → la console
17
+ * affiche un récap au lieu de se figer (budget borné, dégradable — règle
18
+ * observabilité « superviser ≠ tomber la prod »).
19
+ *
20
+ * **Lazy par construction** (créé par le hub au 1ᵉʳ abonné, `dispose` au dernier) :
21
+ * tant qu'aucun auditeur n'écoute `nodefony:audit`, ce pont N'EXISTE PAS — aucun
22
+ * listener sur l'`AuditService`, aucun timer. Au repos avec auditeur connecté mais
23
+ * sans événement : ring `null`, 0 timer (armé au 1ᵉʳ événement, `unref`).
24
+ *
25
+ * @param source - l'`AuditService` (slot `subscribe`).
26
+ * @param publish - publication hub (le canal est fourni par la factory).
27
+ * @param channel - canal de publication (`nodefony:audit`).
28
+ * @returns dispose() — détache le listener `AuditService` ET désarme le timer.
29
+ * OBLIGATOIRE (aucun listener/timer sans cleanup, sinon fuite à chaque
30
+ * dernier désabonnement).
31
+ */
32
+ function createAuditBridge(source, publish, channel, opts = {}) {
33
+ const flushMs = opts.flushMs ?? 250;
34
+ const maxBatch = opts.maxBatch ?? 200;
35
+ let ring = null;
36
+ let head = 0;
37
+ let count = 0;
38
+ let dropped = 0;
39
+ let timer = null;
40
+ const flush = () => {
41
+ timer = null;
42
+ if (count === 0 && dropped === 0) return;
43
+ const events = new Array(count);
44
+ for (let i = 0; i < count; i++) events[i] = ring[(head + i) % maxBatch];
45
+ const d = dropped;
46
+ for (let i = 0; i < maxBatch; i++) ring[i] = void 0;
47
+ head = 0;
48
+ count = 0;
49
+ dropped = 0;
50
+ publish(channel, {
51
+ events,
52
+ dropped: d
53
+ });
54
+ };
55
+ const onEvent = (event) => {
56
+ if (ring === null) ring = new Array(maxBatch);
57
+ if (count === maxBatch) {
58
+ ring[head] = event;
59
+ head = (head + 1) % maxBatch;
60
+ dropped++;
61
+ } else {
62
+ ring[(head + count) % maxBatch] = event;
63
+ count++;
64
+ }
65
+ if (timer === null) {
66
+ timer = setTimeout(flush, flushMs);
67
+ timer.unref?.();
68
+ }
69
+ };
70
+ const unsubscribe = source.subscribe(onEvent);
71
+ return () => {
72
+ if (timer) {
73
+ clearTimeout(timer);
74
+ timer = null;
75
+ }
76
+ ring = null;
77
+ head = count = dropped = 0;
78
+ unsubscribe();
79
+ };
80
+ }
81
+ //#endregion
82
+ export { SECURITY_AUDIT_CHANNEL, createAuditBridge, createAuditBridge as default };
@@ -0,0 +1,60 @@
1
+ //#region nodefony/src/audit/auditFilters.ts
2
+ /**
3
+ * **Le vocabulaire de filtre du journal d'audit**, en noms PUBLICS — ceux qu'un
4
+ * auditeur écrit dans l'URL (`?category=authz&outcome=denied&since=…`).
5
+ *
6
+ * Les deux énumérations y sont écrites en toutes lettres, et
7
+ * {@link AUDIT_FILTER_VOCABULARY_IS_COMPLETE} vérifie **à la compilation**
8
+ * qu'elles couvrent exactement `AuditCategory` et `AuditOutcome`.
9
+ *
10
+ * Ce contrôle n'est pas décoratif : la liste qu'il remplace avait DÉJÀ dérivé.
11
+ * Un `Set` recopié à la main dans le data plane portait dix catégories quand le
12
+ * type en déclarait onze — `?category=config` tombait donc hors de l'allowlist,
13
+ * était ignoré en silence, et l'auditeur recevait le journal ENTIER en croyant
14
+ * lire les seules mutations de configuration. Une liste recopiée ne diverge
15
+ * jamais bruyamment.
16
+ *
17
+ * `actor`, `action` et `requestId` restent des chaînes libres : ce sont des
18
+ * identifiants produits à l'exécution, aucune allowlist ne peut les connaître.
19
+ * `since`/`until` sont des horodatages en millisecondes (bornes incluses).
20
+ */
21
+ const AUDIT_FILTERS = {
22
+ /** Famille d'événement — la liste EST le type `AuditCategory`. */
23
+ category: [
24
+ "auth",
25
+ "authz",
26
+ "token",
27
+ "session",
28
+ "oauth",
29
+ "webauthn",
30
+ "csrf",
31
+ "cors",
32
+ "ws",
33
+ "webhook",
34
+ "config"
35
+ ],
36
+ /** Issue — `denied` est le signal d'accès non autorisé. */
37
+ outcome: [
38
+ "success",
39
+ "failure",
40
+ "denied"
41
+ ],
42
+ /** Identité de l'acteur (égalité stricte). */
43
+ actor: "string",
44
+ /** Nom de l'action auditée (égalité stricte). */
45
+ action: "string",
46
+ /** Corrèle toutes les traces d'une même requête. */
47
+ requestId: "string",
48
+ /** Borne basse, horodatage en millisecondes. */
49
+ since: "int",
50
+ /** Borne haute, horodatage en millisecondes. */
51
+ until: "int"
52
+ };
53
+ /**
54
+ * Preuve **à la compilation** que le vocabulaire ci-dessus est exactement celui
55
+ * des types du contrat. Elle remplace la discipline humaine « penser à mettre
56
+ * les deux à jour », qui avait échoué en silence.
57
+ */
58
+ const AUDIT_FILTER_VOCABULARY_IS_COMPLETE = [true, true];
59
+ //#endregion
60
+ export { AUDIT_FILTERS, AUDIT_FILTER_VOCABULARY_IS_COMPLETE };
@@ -0,0 +1,25 @@
1
+ import { MemoryAuditStore } from "./MemoryAuditStore.js";
2
+ //#region nodefony/src/audit/auditStoreRegistry.ts
3
+ const factories = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Enregistre (ou remplace) la fabrique d'un store d'audit. Appelée par le builtin
6
+ * `memory` au chargement, et par les adapters (drizzle/mongoose/redis) pour les leurs.
7
+ */
8
+ function registerAuditStore(name, factory) {
9
+ factories.set(name, factory);
10
+ }
11
+ /** Fabrique d'un store par nom, ou `undefined` si inconnu. */
12
+ function getAuditStoreFactory(name) {
13
+ return factories.get(name);
14
+ }
15
+ /** Noms enregistrés (validation boot, introspection Studio, tests). */
16
+ function listAuditStores() {
17
+ return [...factories.keys()];
18
+ }
19
+ const MS_PER_DAY = 864e5;
20
+ registerAuditStore("memory", (ctx) => {
21
+ const days = ctx?.config?.audit?.retentionDays;
22
+ return typeof days === "number" ? new MemoryAuditStore(Date.now, days * MS_PER_DAY) : new MemoryAuditStore();
23
+ });
24
+ //#endregion
25
+ export { getAuditStoreFactory, listAuditStores, registerAuditStore };
@@ -0,0 +1,24 @@
1
+ //#region nodefony/src/audit/readAuditContext.ts
2
+ /**
3
+ * Extrait IP / User-Agent / requestId + drapeaux de présence (jamais la valeur)
4
+ * d'un contexte de requête — pour enrichir un événement d'audit avec la
5
+ * provenance (« d'où vient cette tentative »). Calque {@link JsonAuditLogger}.
6
+ *
7
+ * @param context - contexte HTTP/WS courant (typé `unknown` à la frontière).
8
+ * @returns provenance normalisée ; champs `null` si l'info est absente.
9
+ */
10
+ function readAuditContext(context) {
11
+ const ctx = context ?? {};
12
+ const headers = ctx.request?.headers ?? {};
13
+ return {
14
+ ip: ctx.remoteAddress ?? null,
15
+ userAgent: ctx.getUserAgent?.() ?? null,
16
+ requestId: ctx.requestId ?? null,
17
+ flags: {
18
+ hasAuthorization: Boolean(headers["authorization"]),
19
+ hasCookie: Boolean(headers["cookie"])
20
+ }
21
+ };
22
+ }
23
+ //#endregion
24
+ export { readAuditContext };
@@ -0,0 +1,16 @@
1
+ //#region nodefony/src/audit/recordAudit.ts
2
+ /**
3
+ * Émet un événement d'audit **si** le service est présent — no-op sinon. La
4
+ * résolution se fait par le container (`auditService`) sur le **cold-path**
5
+ * (login, refus, révocation) : le coût d'un `Map.get` y est négligeable, et le
6
+ * journal reste **découplé** (module audit absent ou désactivé → aucun effet,
7
+ * jamais d'exception qui remonterait dans le flux métier).
8
+ *
9
+ * @param container - container du service émetteur (`this.container`).
10
+ * @param event - brouillon d'événement (l'`AuditService` pose `id` + `ts`).
11
+ */
12
+ function recordAudit(container, event) {
13
+ (container?.get("auditService"))?.record(event);
14
+ }
15
+ //#endregion
16
+ export { recordAudit };
@@ -0,0 +1,36 @@
1
+ import { AnonymousToken } from "../token/AnonymousToken.js";
2
+ //#region nodefony/src/authenticator/AnonymousAuthenticator.ts
3
+ /**
4
+ * Acceptation EXPLICITE de l'anonymat dans une zone — le seul authenticator
5
+ * autorisé à produire un token non authentifié sans déclencher le Zero Trust.
6
+ *
7
+ * Ne le lister que volontairement : une zone `authenticators: ["jwt", "anonymous"]`
8
+ * (mode `first`) signifie « identifié si preuve présente, sinon visiteur anonyme
9
+ * accepté ». Sans lui, zone protégée + aucune preuve → 401. En mode `all` il
10
+ * reste utile en DERNIER : « le canal doit être prouvé (ex. mtls), l'identité
11
+ * utilisateur est optionnelle ».
12
+ *
13
+ * Zéro coût : `supports()` accepte tout, le token porte le singleton gelé
14
+ * `anonymousUser` (aucune allocation d'utilisateur).
15
+ */
16
+ var AnonymousAuthenticator = class {
17
+ name = "anonymous";
18
+ supports() {
19
+ return true;
20
+ }
21
+ createToken() {
22
+ return Promise.resolve(new AnonymousToken());
23
+ }
24
+ /** Toujours un succès — accepter l'anonymat ne vérifie rien. */
25
+ authenticate(token) {
26
+ return Promise.resolve(token);
27
+ }
28
+ onSuccess(_context, _token) {
29
+ return Promise.resolve();
30
+ }
31
+ onFailure(_context, _error) {
32
+ return Promise.resolve();
33
+ }
34
+ };
35
+ //#endregion
36
+ export { AnonymousAuthenticator, AnonymousAuthenticator as default };
@@ -0,0 +1,164 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { UserToken } from "../token/UserToken.js";
3
+ import { bearerToken } from "./bearer.js";
4
+ import { looksLikeApiKey, parseApiKey } from "../apikey/apiKeyFormat.js";
5
+ //#region nodefony/src/authenticator/ApiKeyAuthenticator.ts
6
+ const INVALID_TOKEN = "Invalid token";
7
+ /**
8
+ * Marqueur interne posé par `authenticate()` et consommé par `onSuccess()` :
9
+ * l'horodatage à inscrire, quand la fenêtre de throttle est dépassée. Il ne
10
+ * traverse jamais le pipeline — l'attribut vit sur le token de la requête.
11
+ */
12
+ const MARK_USED_AT = "apiKeyMarkUsedAt";
13
+ /**
14
+ * Authentification par **clé API personnelle (PAT, P6.12)** présentée en
15
+ * `Authorization: Bearer <prefix>_…` (RFC 6750). Réservée API/CI/scripts — le web
16
+ * utilise la session BFF.
17
+ *
18
+ * Un PAT est un **bearer opaque** (≠ JWT auto-porté) : sa vérité vit côté serveur
19
+ * (`ITokenStore`), donc il est **révocable immédiatement**. Discrimination du
20
+ * JWT : le PAT porte le préfixe `<prefix>_` (le JWT a la structure compacte
21
+ * `a.b.c`) → les deux authenticators cohabitent dans une même zone.
22
+ *
23
+ * Défenses :
24
+ * - **forme + CRC validés AVANT tout accès au store** ({@link parseApiKey}) →
25
+ * une valeur malformée n'atteint jamais la base (anti-DoS) ;
26
+ * - lookup par **hash** (`sha256`) — le secret n'existe nulle part au repos ;
27
+ * - **révocation** immédiate (`revokedAt`) + **expiration** (`expiresAt`) +
28
+ * **ban en masse** du porteur (`invalidBefore` vs `createdAt`) ;
29
+ * - **sujet revérifié** à chaque requête (`loadUserByIdentifier` → disparu/
30
+ * inactif/verrouillé = rejet) — rôles **frais** (révocation effective) ;
31
+ * - message d'échec **uniforme** (anti-énumération).
32
+ *
33
+ * Dépendances (store, userProvider) résolues **paresseusement** du container.
34
+ */
35
+ var ApiKeyAuthenticator = class {
36
+ name = "apikey";
37
+ #container;
38
+ #prefix;
39
+ #throttleMs;
40
+ #store = null;
41
+ #userProvider = null;
42
+ /**
43
+ * @param container - container DI (résolution lazy de `tokenStore`/`users`).
44
+ * @param runtime - préfixe + throttle effectifs (dérivés de `config.apiKeys`).
45
+ */
46
+ constructor(container, runtime) {
47
+ this.#container = container;
48
+ this.#prefix = runtime.prefix;
49
+ this.#throttleMs = Math.max(0, runtime.lastUsedThrottleS) * 1e3;
50
+ }
51
+ /** La requête porte-t-elle un `Authorization: Bearer <prefix>_…` ? (test bon marché) */
52
+ supports(context) {
53
+ const auth = context.request?.headers?.authorization;
54
+ if (typeof auth !== "string") return false;
55
+ const token = bearerToken(auth);
56
+ return token !== null && looksLikeApiKey(token, this.#prefix);
57
+ }
58
+ /** Extrait la valeur brute (non vérifiée) → portée par un `UserToken` type `"apikey"`. */
59
+ createToken(context) {
60
+ const auth = context.request?.headers?.authorization;
61
+ return Promise.resolve(new UserToken("apikey", bearerToken(auth) ?? ""));
62
+ }
63
+ /**
64
+ * Valide la clé (forme+CRC, puis store) et résout le sujet — ou lève un 401 au
65
+ * message uniforme.
66
+ *
67
+ * @throws AuthenticationError (401) — clé malformée/inconnue/révoquée/expirée,
68
+ * ou sujet disparu/banni.
69
+ * @throws Error (câblage : store/users absents) — loggée ERROR par le firewall
70
+ * puis 401 fail-closed (rien ne fuite au client).
71
+ */
72
+ async authenticate(token) {
73
+ const raw = token.getCredentials();
74
+ if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
75
+ const parsed = parseApiKey(raw, this.#prefix);
76
+ if (parsed === null) throw new AuthenticationError(INVALID_TOKEN);
77
+ const store = this.#resolveStore();
78
+ const record = await store.findByHash(parsed.secretHash);
79
+ const now = Date.now();
80
+ if (!record || record.kind !== "pat" || record.revokedAt !== null || record.expiresAt !== null && record.expiresAt <= now) throw new AuthenticationError(INVALID_TOKEN);
81
+ const invalidBefore = await store.getInvalidBefore(record.subjectId);
82
+ if (invalidBefore !== null && record.createdAt < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
83
+ const user = await this.#resolveUserOrReject(record.subjectId);
84
+ const last = record.lastUsedAt;
85
+ if (this.#throttleMs === 0 || last === null || now - last >= this.#throttleMs) token.setAttribute(MARK_USED_AT, now);
86
+ const ut = token;
87
+ ut.promote(user);
88
+ ut.setAttribute("scopes", [...record.scopes]);
89
+ ut.setAttribute("apiKeyId", record.id);
90
+ ut.setAttribute("tenantId", record.tenantId);
91
+ return ut;
92
+ }
93
+ /**
94
+ * Inscrit la trace d'usage de la clé — horodatage, IP et agent.
95
+ *
96
+ * C'est ici, et pas dans `authenticate()`, parce que c'est ici qu'on reçoit
97
+ * le contexte. La provenance se lit par les ACCESSEURS proxy-aware des
98
+ * contextes concrets (`getRemoteAddress()` dépouille `X-Forwarded-For` selon
99
+ * `trustProxy`), absents du type de base — duck-typing optionnel, même
100
+ * approche que `AuthFlow.#openSession()`.
101
+ *
102
+ * Rien n'est écrit si `authenticate()` n'a pas posé le marqueur : la fenêtre
103
+ * de throttle n'était pas dépassée, et le hot path reste sans écriture.
104
+ */
105
+ async onSuccess(context, token) {
106
+ const at = token.getAttribute(MARK_USED_AT);
107
+ if (at === void 0) return;
108
+ const id = token.getAttribute("apiKeyId");
109
+ if (id === void 0) return;
110
+ const provenance = context;
111
+ let ip;
112
+ let userAgent;
113
+ try {
114
+ ip = provenance.getRemoteAddress?.() ?? void 0;
115
+ userAgent = provenance.getUserAgent?.();
116
+ } catch {}
117
+ try {
118
+ await this.#resolveStore().markUsed(id, {
119
+ at,
120
+ ip,
121
+ userAgent
122
+ });
123
+ } catch (e) {
124
+ context.log(`apikey: trace d'usage non enregistrée (${id}) — ${String(e)}`, "WARNING");
125
+ }
126
+ }
127
+ /** Slot audit (P6.14) — le 401 + challenge sont posés par le firewall. */
128
+ onFailure(_context, _error) {
129
+ return Promise.resolve();
130
+ }
131
+ /** Challenge RFC 6750/7235 posé par le firewall sur les 401 de la zone. */
132
+ challenge() {
133
+ return "Bearer";
134
+ }
135
+ async #resolveUserOrReject(sub) {
136
+ const provider = this.#resolveUserProvider();
137
+ let user;
138
+ try {
139
+ user = await provider.loadUserByIdentifier(sub);
140
+ } catch {
141
+ throw new AuthenticationError(INVALID_TOKEN);
142
+ }
143
+ if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
144
+ return user;
145
+ }
146
+ #resolveStore() {
147
+ if (this.#store === null) {
148
+ const store = this.#container.get("tokenStore");
149
+ if (!store) throw new Error("ApiKeyAuthenticator: service 'tokenStore' absent du container — le TokenService de @nodefony/security doit être chargé.");
150
+ this.#store = store;
151
+ }
152
+ return this.#store;
153
+ }
154
+ #resolveUserProvider() {
155
+ if (this.#userProvider === null) {
156
+ const provider = this.#container.get("users");
157
+ if (!provider) throw new Error("ApiKeyAuthenticator: aucun service 'users' (IUserProvider) dans le container — enregistrer un UserService au boot de l'application.");
158
+ this.#userProvider = provider;
159
+ }
160
+ return this.#userProvider;
161
+ }
162
+ };
163
+ //#endregion
164
+ export { ApiKeyAuthenticator, ApiKeyAuthenticator as default };
@@ -0,0 +1,224 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { UnverifiableTokenError } from "../../errors/UnverifiableTokenError.js";
3
+ import { UserToken } from "../token/UserToken.js";
4
+ import { bearerToken } from "./bearer.js";
5
+ import peekIssuer from "./peekIssuer.js";
6
+ import { localIdentifierFor } from "./externalSubject.js";
7
+ import { ACCESS_TOKEN_VERIFIER, buildBearerChallenge, canonicalIssuer, protectedResourceMetadataUrl } from "nodefony";
8
+ import { BaseUser } from "@nodefony/user";
9
+ //#region nodefony/src/authenticator/ExternalJwtAuthenticator.ts
10
+ /**
11
+ * Message UNIFORME de refus — la cause fine (expiré, audience, signature, sujet
12
+ * inconnu) part au journal, jamais au client : un refus détaillé est un oracle
13
+ * qui aide à fabriquer un jeton acceptable.
14
+ */
15
+ const INVALID_TOKEN = "Invalid token";
16
+ /** Clé de transport de l'audience entre `createToken` et `authenticate`. */
17
+ const AUDIENCE = "audience";
18
+ /**
19
+ * Authentification par **jeton d'accès émis par un serveur d'autorisation
20
+ * TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).
21
+ *
22
+ * C'est le chaînon qui relie deux pièces déjà en place : le vérificateur de
23
+ * jetons distants, qui sait lire un jeton dont on ne possède pas la clé, et le
24
+ * pare-feu, qui raisonne en utilisateurs et en rôles. Le vérificateur s'arrête
25
+ * à un sujet et des scopes — délibérément, car établir une identité
26
+ * applicative est une décision de l'application, pas du protocole. C'est cette
27
+ * décision-là que porte cette classe, et rien d'autre.
28
+ *
29
+ * ## Cohabitation avec les jetons maison
30
+ *
31
+ * `JwtAuthenticator` et celui-ci reconnaissent la même forme de credential.
32
+ * Chacun ne prend donc que les jetons dont l'émetteur revendiqué est le sien
33
+ * ({@link peekIssuer}) — lecture non vérifiée qui ne sert qu'à AIGUILLER. Sans
34
+ * cela, en mode `first`, le premier listé capturerait les deux familles et
35
+ * refuserait la moitié des jetons : l'ordre de la configuration deviendrait
36
+ * une décision de sécurité, dont l'erreur ne se verrait qu'en production.
37
+ *
38
+ * ## Ce qui vaut garantie
39
+ *
40
+ * - **L'audience vient de la ZONE**, jamais du jeton, et elle est obligatoire :
41
+ * sans elle l'authenticator refuse de démarrer ({@link validateArea}).
42
+ * - **Un refus est un 401 uniforme ; une PANNE est un 503** — un émetteur
43
+ * injoignable n'est pas un jeton invalide, et le dire autrement enverrait le
44
+ * client renouveler en boucle un jeton parfaitement bon.
45
+ * - **Le sujet est revérifié localement** en mode `require` : un compte
46
+ * supprimé, désactivé ou verrouillé ferme l'accès sans attendre l'expiration
47
+ * du jeton, que l'application ne contrôle pas.
48
+ *
49
+ * - **Le sujet n'entre jamais nu dans l'espace de noms local** : un `sub` n'est
50
+ * unique que chez son émetteur, et le rattachement passe donc par
51
+ * {@link localIdentifierFor}, piloté par le `subjectMapping` de CET émetteur.
52
+ * - **Le refus est apprenable** : le défi porte le pointeur `resource_metadata`
53
+ * (RFC 9728), qui dit au client où aller chercher de quoi obtenir un jeton.
54
+ */
55
+ var ExternalJwtAuthenticator = class {
56
+ name = "external-jwt";
57
+ #container;
58
+ #issuers;
59
+ #policy;
60
+ #ephemeralRoles;
61
+ #userProvider = null;
62
+ /**
63
+ * @param container - container DI (résolution lazy du vérificateur et de `users`)
64
+ * @param options - émetteurs reconnus et politique de rattachement
65
+ */
66
+ constructor(container, options) {
67
+ this.#container = container;
68
+ const issuers = /* @__PURE__ */ new Map();
69
+ for (const binding of options.issuers) try {
70
+ issuers.set(canonicalIssuer(binding.issuer), binding.subjectMapping);
71
+ } catch {
72
+ continue;
73
+ }
74
+ this.#issuers = issuers;
75
+ this.#policy = options.subjectPolicy;
76
+ this.#ephemeralRoles = options.ephemeralRoles;
77
+ }
78
+ /**
79
+ * La requête porte-t-elle un jeton se réclamant d'un émetteur de confiance ?
80
+ *
81
+ * Le contrôle est délibérément le MÊME que celui du vérificateur, et non un
82
+ * simple « c'est un JWT » : un jeton maison ne doit pas être capturé ici, et
83
+ * un jeton d'un émetteur inconnu n'a pas à provoquer le moindre travail.
84
+ */
85
+ supports(context) {
86
+ const auth = context.request?.headers?.authorization;
87
+ if (typeof auth !== "string") return false;
88
+ const raw = bearerToken(auth);
89
+ if (raw === null) return false;
90
+ const issuer = peekIssuer(raw);
91
+ if (issuer === null) return false;
92
+ if (this.#issuers.has(issuer)) return true;
93
+ try {
94
+ return this.#issuers.has(canonicalIssuer(issuer));
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Extrait le jeton brut ET l'audience de la zone.
101
+ *
102
+ * L'audience transite par le token parce que `authenticate()` ne reçoit pas
103
+ * le contexte : c'est ici, et seulement ici, qu'on sait quelle ressource est
104
+ * visée.
105
+ */
106
+ createToken(context) {
107
+ const auth = context.request?.headers?.authorization;
108
+ const token = new UserToken("external-jwt", bearerToken(auth) ?? "");
109
+ const area = context.security;
110
+ if (area?.resource) token.setAttribute(AUDIENCE, area.resource);
111
+ return Promise.resolve(token);
112
+ }
113
+ /**
114
+ * Vérifie le jeton auprès de son émetteur, puis rattache le sujet.
115
+ *
116
+ * @throws AuthenticationError (401) — jeton refusé, ou sujet sans compte
117
+ * local utilisable
118
+ * @throws UnverifiableTokenError (503) — rien ne peut vérifier ce jeton, ou
119
+ * l'émetteur est injoignable : on ne sait pas, et on le dit
120
+ */
121
+ async authenticate(token) {
122
+ const raw = token.getCredentials();
123
+ if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
124
+ const audience = token.getAttribute(AUDIENCE);
125
+ if (!audience) throw new UnverifiableTokenError();
126
+ const verify = this.#container.get(ACCESS_TOKEN_VERIFIER);
127
+ if (typeof verify !== "function") throw new UnverifiableTokenError();
128
+ let principal;
129
+ try {
130
+ principal = await verify(raw, audience);
131
+ } catch (error) {
132
+ throw new UnverifiableTokenError(error.message);
133
+ }
134
+ if (principal === null) throw new AuthenticationError(INVALID_TOKEN);
135
+ const subject = principal.subject;
136
+ if (!subject) throw new AuthenticationError(INVALID_TOKEN);
137
+ const mapping = this.#issuers.get(principal.issuer);
138
+ if (mapping === void 0) throw new UnverifiableTokenError(`émetteur « ${principal.issuer} » vérifié mais absent de la table de rattachement des sujets — configuration incohérente.`);
139
+ const user = await this.#resolveUser(principal.issuer, subject, mapping);
140
+ const ut = token;
141
+ ut.promote(user);
142
+ ut.setAttribute("scopes", [...principal.scopes]);
143
+ ut.setAttribute("subject", subject);
144
+ ut.setAttribute("issuer", principal.issuer);
145
+ if (principal.expiresAt !== void 0 || principal.issuedAt !== void 0) ut.setAttribute("claims", {
146
+ exp: principal.expiresAt,
147
+ iat: principal.issuedAt
148
+ });
149
+ if (principal.tokenId !== void 0) ut.setAttribute("jti", principal.tokenId);
150
+ return ut;
151
+ }
152
+ /** Slot audit — le firewall enregistre déjà succès et échec par zone. */
153
+ onSuccess(_context, _token) {
154
+ return Promise.resolve();
155
+ }
156
+ /** Slot audit — le 401 et le défi sont posés par le firewall. */
157
+ onFailure(_context, _error) {
158
+ return Promise.resolve();
159
+ }
160
+ /**
161
+ * Défi RFC 6750 + pointeur RFC 9728, posé par le firewall sur les 401.
162
+ *
163
+ * ⭐ **C'est cet en-tête qui rend l'autorisation apprenable.** Un `Bearer` nu
164
+ * est un mur : le client sait qu'il lui faut un jeton, mais pas où le
165
+ * demander. `resource_metadata` nomme le document qui le lui dira — c'est le
166
+ * seul mécanisme normalisé pour ça, et celui qu'un client MCP conforme suit.
167
+ *
168
+ * Aucun `error` n'est joint : le firewall pose ce défi sur TOUT 401 de la
169
+ * zone, sans savoir si la requête portait un jeton. Or la RFC 6750 §3
170
+ * demande de ne PAS mettre de code d'erreur quand elle n'en portait aucun —
171
+ * un `invalid_token` ferait renouveler en boucle un jeton qui n'existe pas.
172
+ *
173
+ * @param area - zone refusante ; sans elle (ou sans ressource déclarée) le
174
+ * défi retombe sur `Bearer` nu, faute de ressource à nommer
175
+ */
176
+ challenge(area) {
177
+ if (!area?.resource) return "Bearer";
178
+ return buildBearerChallenge({ resourceMetadataUrl: protectedResourceMetadataUrl(area.resource) });
179
+ }
180
+ /**
181
+ * Refuse une zone sans ressource — au boot, pas à la première requête.
182
+ *
183
+ * Sans audience, la vérification accepterait un jeton émis pour un autre
184
+ * service : le seul verrou qui lie un jeton à CE service disparaîtrait, et
185
+ * l'application n'en saurait rien.
186
+ */
187
+ validateArea(area) {
188
+ if (!area.resource) throw new Error(`area "${area.name}": l'authenticator "${this.name}" exige que la zone déclare sa ressource (\`resource\`) — c'est l'audience que le jeton doit porter (RFC 8707 §2). Sans elle, un jeton valide délivré au même porteur pour un AUTRE service serait accepté ici.`);
189
+ }
190
+ /**
191
+ * Établit l'utilisateur applicatif à partir du sujet du jeton.
192
+ *
193
+ * Les deux politiques répondent à deux questions différentes : « qui, chez
194
+ * nous, est cette personne ? » et « quel pouvoir accorde-t-on à une machine
195
+ * que l'annuaire a authentifiée ? ».
196
+ */
197
+ async #resolveUser(issuer, subject, mapping) {
198
+ const identifier = localIdentifierFor(issuer, subject, mapping);
199
+ if (this.#policy === "ephemeral") return new BaseUser({
200
+ id: identifier,
201
+ identifier,
202
+ roles: [...this.#ephemeralRoles]
203
+ });
204
+ const provider = this.#resolveUserProvider();
205
+ let user;
206
+ try {
207
+ user = await provider.loadUserByIdentifier(identifier);
208
+ } catch {
209
+ throw new AuthenticationError(INVALID_TOKEN);
210
+ }
211
+ if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
212
+ return user;
213
+ }
214
+ #resolveUserProvider() {
215
+ if (this.#userProvider === null) {
216
+ const provider = this.#container.get("users");
217
+ if (!provider) throw new Error("ExternalJwtAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot, ou passer la politique de sujet à \"ephemeral\" si l'appelant est une machine sans compte local.");
218
+ this.#userProvider = provider;
219
+ }
220
+ return this.#userProvider;
221
+ }
222
+ };
223
+ //#endregion
224
+ export { ExternalJwtAuthenticator, ExternalJwtAuthenticator as default };