@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,343 @@
1
+ import { AuthenticationError } from "../errors/AuthenticationError.js";
2
+ import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
+ import { WebAuthnError } from "../errors/WebAuthnError.js";
4
+ import { getWebAuthnStoreFactory, listWebAuthnStores } from "../src/webauthn/webAuthnCredentialStoreRegistry.js";
5
+ import { AUTO_STORE, EMPTY_INFRA, Service, deriveStoreBackend, readStoreLocation, resolveAutoStore } from "nodefony";
6
+ import { Buffer } from "node:buffer";
7
+ //#region nodefony/service/webAuthn.ts
8
+ const serviceName = "webauthn";
9
+ function isFlushable(s) {
10
+ return s !== null && typeof s.flushNow === "function";
11
+ }
12
+ /**
13
+ * **WebAuthn / passkeys** (P6 J9) — orchestrateur des deux cérémonies FIDO2
14
+ * (WebAuthn L3 §7.1 enregistrement, §7.2 authentification).
15
+ *
16
+ * MFA **phishing-resistant** : la clé privée ne quitte jamais l'authenticator
17
+ * (Touch ID, Windows Hello, clé FIDO). Le serveur ne manipule QUE des clés
18
+ * publiques + vérifie des signatures. La vérification cryptographique (parsing
19
+ * CBOR/COSE, signatures ES256/RS256/EdDSA) est déléguée à `@simplewebauthn/server`
20
+ * (lib auditée de l'écosystème), **importée paresseusement** au 1ᵉʳ usage (cold
21
+ * path — l'enregistrement/login n'est pas le hot path par requête).
22
+ *
23
+ * Au boot (si `passkeys.enabled`) : résout le RP (rpID/rpName/origines depuis la
24
+ * config, sinon le domaine de l'app) + le store de credentials pluggable
25
+ * (`webAuthnCredentialStore` du container, sinon le builtin mémoire) et le pose
26
+ * au container — une application peut donc fournir le sien avant le boot.
27
+ *
28
+ * Il n'existe **pas** d'authenticator passkey : `WebAuthnController`
29
+ * (`@nodefony/framework`) mène les deux cérémonies via ce service, puis
30
+ * `AuthFlow.establishSessionFor()` ouvre la session BFF ; les requêtes
31
+ * suivantes sont ré-authentifiées par `SessionAuthenticator`.
32
+ *
33
+ * **Anti-rejeu** : le challenge serveur est porté HORS de ce service (en session
34
+ * BFF par le controller) ; chaque `verify*` reçoit le `expectedChallenge` qu'il
35
+ * a émis — un challenge n'est jamais réutilisable.
36
+ */
37
+ var WebAuthnService = class extends Service {
38
+ module;
39
+ #config = null;
40
+ #lib = null;
41
+ #store = null;
42
+ #rpID = "localhost";
43
+ #rpName = "Nodefony";
44
+ #ready = false;
45
+ constructor(module) {
46
+ super(serviceName, module.container, module.notificationsCenter, module.options);
47
+ this.module = module;
48
+ this.kernel?.once("onBoot", () => this.#build());
49
+ this.kernel?.once("onTerminate", () => void this.#shutdown());
50
+ }
51
+ #build() {
52
+ let config;
53
+ try {
54
+ config = defineSecurityConfig(this.options);
55
+ } catch {
56
+ return;
57
+ }
58
+ if (!config.passkeys.enabled) {
59
+ this.log("webauthn idle — passkeys désactivés en config", "DEBUG");
60
+ return;
61
+ }
62
+ this.#config = config;
63
+ const rawDomain = config.passkeys.rpId ?? (this.kernel?.domain || "localhost");
64
+ this.#rpID = /^(\d{1,3}\.){3}\d{1,3}$|:/.test(rawDomain) ? "localhost" : rawDomain;
65
+ this.#rpName = config.passkeys.rpName ?? "Nodefony";
66
+ const existing = this.get("webAuthnCredentialStore");
67
+ let resolved;
68
+ let reason;
69
+ if (existing) {
70
+ this.#store = existing;
71
+ resolved = deriveStoreBackend(existing);
72
+ reason = "adapter posé au container (infra database déclarée)";
73
+ } else {
74
+ let driver = config.passkeys.store;
75
+ reason = `store explicitement configuré ("${driver}")`;
76
+ if (driver === AUTO_STORE) {
77
+ const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listWebAuthnStores());
78
+ driver = auto.store;
79
+ reason = auto.reason;
80
+ this.log(`passkeys.store "auto" → "${driver}" (${auto.reason})`, "INFO");
81
+ }
82
+ const factory = getWebAuthnStoreFactory(driver);
83
+ if (!factory) {
84
+ const msg = `webauthn store "${driver}" inconnu (enregistrés : ${listWebAuthnStores().join(", ") || "aucun"})`;
85
+ if (this.kernel?.environment === "production") throw new Error(`${msg} — passkeys indisponibles : boot avorté.`);
86
+ this.log(`${msg} — passkeys indisponibles`, "CRITIC");
87
+ return;
88
+ }
89
+ if (driver === "memory" && this.kernel?.environment === "production") this.log("passkeys.store \"memory\" en PRODUCTION — credentials WebAuthn volatils : tous les passkeys enregistrés sont perdus au redémarrage (utilisateurs verrouillés hors de leur compte). Déclarer une infra durable (NF_DATABASE_URL).", "WARNING");
90
+ this.#store = factory({
91
+ container: this.container,
92
+ config
93
+ });
94
+ this.container?.set("webAuthnCredentialStore", this.#store);
95
+ resolved = driver;
96
+ }
97
+ this.kernel?.registerStoreResolution({
98
+ brick: "passkeys",
99
+ nature: "durable",
100
+ configured: config.passkeys.store,
101
+ resolved,
102
+ available: listWebAuthnStores(),
103
+ reason,
104
+ configPath: "security.passkeys.store",
105
+ location: readStoreLocation(this.#store)
106
+ });
107
+ if (config.passkeys.attestation !== "none") this.log(`passkeys.attestation "${config.passkeys.attestation}" — l'attestation est demandée au navigateur mais N'EST PAS VÉRIFIÉE : ni métadonnées FIDO (MDS), ni certificats racines fabricant, et l'AAGUID n'est pas conservé. Ce réglage ne tient donc pas un AAL3 régulé. Repasser à "none" si la garantie n'est pas requise.`, "WARNING");
108
+ if (config.passkeys.residentKey === "discouraged") this.log("passkeys.residentKey \"discouraged\" — les credentials enrôlés ne sont pas découvrables, or le login par passkey est usernameless (aucun ciblage depuis la requête, anti-énumération). Ces porteurs ne pourront pas se connecter : repasser à \"preferred\" ou \"required\".", "WARNING");
109
+ this.#ready = true;
110
+ this.log(`webauthn ready — rpID "${this.#rpID}", store "${config.passkeys.store}"`, "DEBUG");
111
+ }
112
+ /**
113
+ * Arrêt propre : écrit immédiatement le store sur disque s'il est persistant
114
+ * (driver `file`) → aucune écriture en attente de flush n'est perdue au
115
+ * redémarrage. No-op pour un store mémoire ou un adapter sans `flushNow`.
116
+ */
117
+ async #shutdown() {
118
+ if (isFlushable(this.#store)) try {
119
+ await this.#store.flushNow();
120
+ } catch (e) {
121
+ this.log(e, "ERROR");
122
+ }
123
+ }
124
+ /** `true` si les cérémonies sont opérationnelles (passkeys activés, boot OK). */
125
+ isEnabled() {
126
+ return this.#ready;
127
+ }
128
+ /**
129
+ * Prépare les options de `navigator.credentials.create()` — le défi à signer +
130
+ * les contraintes (RP, type d'attestation, sélection d'authenticator). Le
131
+ * challenge renvoyé doit être stocké côté serveur (session) par l'appelant.
132
+ *
133
+ * `excludeCredentials` liste les passkeys déjà enregistrés du même utilisateur
134
+ * pour empêcher un double enregistrement sur le même authenticator (§7.1).
135
+ */
136
+ async generateRegistrationOptions(user) {
137
+ this.#ensureReady();
138
+ const lib = await this.#ensureLib();
139
+ const pk = this.#config.passkeys;
140
+ const existing = await this.#store.findByUser(user.id);
141
+ return lib.generateRegistrationOptions({
142
+ rpName: this.#rpName,
143
+ rpID: this.#rpID,
144
+ userName: user.name,
145
+ userID: new Uint8Array(Buffer.from(user.id, "utf8")),
146
+ userDisplayName: user.displayName ?? user.name,
147
+ attestationType: pk.attestation,
148
+ timeout: pk.timeoutMs,
149
+ excludeCredentials: existing.map((c) => ({
150
+ id: c.id,
151
+ transports: [...c.transports]
152
+ })),
153
+ authenticatorSelection: {
154
+ residentKey: pk.residentKey,
155
+ userVerification: pk.userVerification,
156
+ ...pk.authenticatorAttachment !== "any" ? { authenticatorAttachment: pk.authenticatorAttachment } : {}
157
+ }
158
+ });
159
+ }
160
+ /**
161
+ * Vérifie la réponse d'enregistrement (challenge, origine, rpIdHash, flags,
162
+ * format d'attestation) et **persiste** le nouveau credential.
163
+ *
164
+ * @param expectedChallenge - le challenge émis par {@link generateRegistrationOptions} (session).
165
+ * @param userId - propriétaire du credential (utilisateur authentifié/en création).
166
+ * @param requestOrigin - origine HTTP de la requête (validée si aucune origine n'est configurée).
167
+ * @throws AuthenticationError (401) — vérification échouée.
168
+ * @throws WebAuthnError (409) — plafond `passkeys.maxPerUser` atteint.
169
+ */
170
+ async verifyRegistration(response, expectedChallenge, userId, requestOrigin) {
171
+ this.#ensureReady();
172
+ const lib = await this.#ensureLib();
173
+ const pk = this.#config.passkeys;
174
+ let verification;
175
+ try {
176
+ verification = await lib.verifyRegistrationResponse({
177
+ response,
178
+ expectedChallenge,
179
+ expectedOrigin: this.#expectedOrigin(requestOrigin),
180
+ expectedRPID: this.#rpID,
181
+ requireUserVerification: pk.userVerification === "required"
182
+ });
183
+ } catch {
184
+ throw new AuthenticationError("WebAuthn registration failed");
185
+ }
186
+ if (!verification.verified || !verification.registrationInfo) throw new AuthenticationError("WebAuthn registration failed");
187
+ if (await this.#store.countByUser(userId) >= pk.maxPerUser) throw new WebAuthnError(`passkey limit reached (${pk.maxPerUser})`, 409);
188
+ const info = verification.registrationInfo;
189
+ const credential = {
190
+ id: info.credential.id,
191
+ userId,
192
+ publicKey: Buffer.from(info.credential.publicKey).toString("base64url"),
193
+ signCount: info.credential.counter,
194
+ transports: info.credential.transports ?? [],
195
+ backupEligible: info.credentialDeviceType === "multiDevice",
196
+ backupState: info.credentialBackedUp,
197
+ uvInitialized: info.userVerified,
198
+ createdAt: Date.now(),
199
+ lastUsedAt: null
200
+ };
201
+ await this.#store.save(credential);
202
+ return credential;
203
+ }
204
+ /**
205
+ * Prépare les options de `navigator.credentials.get()`. Sans `userId`
206
+ * (usernameless) : `allowCredentials` est omis → l'authenticator propose ses
207
+ * passkeys découvrables (UX cible). Avec `userId` : ciblage des credentials
208
+ * connus de cet utilisateur.
209
+ *
210
+ * @param userId - identité **déjà prouvée** (session en cours) et jamais un
211
+ * identifiant reçu d'un appelant non authentifié : `allowCredentials`
212
+ * révélerait alors qu'un compte porte une passkey et **lesquelles**, or un
213
+ * `credentialId` est corrélable entre sites (W3C WebAuthn L3, « Privacy leak
214
+ * via credential IDs »). Le controller BFF applique cette règle —
215
+ * `WebAuthnController.loginOptions()` ne cible que depuis la session.
216
+ */
217
+ async generateAuthenticationOptions(userId) {
218
+ this.#ensureReady();
219
+ const lib = await this.#ensureLib();
220
+ const pk = this.#config.passkeys;
221
+ let allowCredentials;
222
+ if (userId) allowCredentials = (await this.#store.findByUser(userId)).map((c) => ({
223
+ id: c.id,
224
+ transports: [...c.transports]
225
+ }));
226
+ return lib.generateAuthenticationOptions({
227
+ rpID: this.#rpID,
228
+ allowCredentials,
229
+ userVerification: pk.userVerification,
230
+ timeout: pk.timeoutMs
231
+ });
232
+ }
233
+ /**
234
+ * Vérifie une assertion (signature sur `authData ‖ SHA-256(clientDataJSON)`
235
+ * avec la clé publique stockée, §7.2) + applique l'état (compteur anti-clone,
236
+ * sauvegarde, usage). Résout l'utilisateur propriétaire via le credentialId.
237
+ *
238
+ * @param expectedChallenge - le challenge émis par {@link generateAuthenticationOptions} (session).
239
+ * @throws AuthenticationError (401) — credential inconnu ou vérification échouée.
240
+ */
241
+ async verifyAuthentication(response, expectedChallenge, requestOrigin) {
242
+ this.#ensureReady();
243
+ const lib = await this.#ensureLib();
244
+ const pk = this.#config.passkeys;
245
+ const stored = await this.#store.findById(response.id);
246
+ if (!stored) throw new AuthenticationError("WebAuthn authentication failed");
247
+ let verification;
248
+ try {
249
+ verification = await lib.verifyAuthenticationResponse({
250
+ response,
251
+ expectedChallenge,
252
+ expectedOrigin: this.#expectedOrigin(requestOrigin),
253
+ expectedRPID: this.#rpID,
254
+ credential: {
255
+ id: stored.id,
256
+ publicKey: new Uint8Array(Buffer.from(stored.publicKey, "base64url")),
257
+ counter: stored.signCount,
258
+ transports: [...stored.transports]
259
+ },
260
+ requireUserVerification: pk.userVerification === "required"
261
+ });
262
+ } catch {
263
+ throw new AuthenticationError("WebAuthn authentication failed");
264
+ }
265
+ if (!verification.verified) throw new AuthenticationError("WebAuthn authentication failed");
266
+ const info = verification.authenticationInfo;
267
+ await this.#store.update(stored.id, {
268
+ signCount: info.newCounter,
269
+ backupState: info.credentialBackedUp,
270
+ uvInitialized: stored.uvInitialized || info.userVerified,
271
+ lastUsedAt: Date.now()
272
+ });
273
+ return {
274
+ credential: stored,
275
+ userId: stored.userId
276
+ };
277
+ }
278
+ /** Liste les credentials d'un utilisateur (UX « mes appareils »). */
279
+ listUserCredentials(userId) {
280
+ this.#ensureReady();
281
+ return this.#store.findByUser(userId);
282
+ }
283
+ /**
284
+ * Page de passkeys pour le data plane admin (vue TRANSVERSE : « quels appareils
285
+ * portent des passkeys sur toute la plateforme »).
286
+ *
287
+ * ≠ {@link listUserCredentials}, qui sert la fiche d'UN utilisateur et le chemin
288
+ * chaud du login. Ici on ne matérialise jamais plus d'une page, et la projection
289
+ * du store exclut la clé publique.
290
+ */
291
+ listCredentialsPage(query) {
292
+ this.#ensureReady();
293
+ return this.#store.listPage(query);
294
+ }
295
+ /**
296
+ * Nombre de passkeys correspondant aux filtres, ou `-1` si le backend ne sait
297
+ * pas compter à coût raisonnable (Redis).
298
+ */
299
+ countCredentials(query) {
300
+ this.#ensureReady();
301
+ return this.#store.countCredentials(query);
302
+ }
303
+ /** Révoque un credential (retrait d'un appareil). */
304
+ removeCredential(credentialId) {
305
+ this.#ensureReady();
306
+ return this.#store.delete(credentialId);
307
+ }
308
+ /**
309
+ * Supprime un credential **du propriétaire** (self-service, anti-IDOR) : la
310
+ * suppression n'aboutit que si le credential appartient bien à `userId`, sinon
311
+ * `false` — 404 indiscernable côté client (on ne révèle pas l'existence d'un
312
+ * credential d'autrui).
313
+ */
314
+ async removeUserCredential(userId, credentialId) {
315
+ this.#ensureReady();
316
+ const cred = await this.#store.findById(credentialId);
317
+ if (!cred || cred.userId !== userId) return false;
318
+ await this.#store.delete(credentialId);
319
+ return true;
320
+ }
321
+ /**
322
+ * Origine(s) attendue(s) (anti-phishing, §7.1/§7.2). Liste blanche de config en
323
+ * priorité (prod). À défaut : l'origine de la requête est acceptée **seulement
324
+ * si son hostname == rpID** (dev : `localhost:port` quel que soit le port, sans
325
+ * jamais ouvrir à un domaine tiers). Dernier recours : `https://{rpID}`.
326
+ */
327
+ #expectedOrigin(requestOrigin) {
328
+ const origins = this.#config.passkeys.origins;
329
+ if (origins.length > 0) return [...origins];
330
+ if (requestOrigin) try {
331
+ if (new URL(requestOrigin).hostname === this.#rpID) return requestOrigin;
332
+ } catch {}
333
+ return `https://${this.#rpID}`;
334
+ }
335
+ async #ensureLib() {
336
+ return this.#lib ??= await import("@simplewebauthn/server");
337
+ }
338
+ #ensureReady() {
339
+ if (!this.#ready || !this.#store || !this.#config) throw new Error("WebAuthnService: non initialisé (passkeys désactivés ou boot échoué)");
340
+ }
341
+ };
342
+ //#endregion
343
+ export { WebAuthnService, WebAuthnService as default };