@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,43 @@
1
+ import type { JSONWebKeySet } from "jose";
2
+ import type { IJwtKeystore, IJwtSigningKey } from "../../contracts/IJwtKeystore.js";
3
+ /** Journalisation injectée (le keystore n'est pas un Service — il reçoit un log). */
4
+ type LogFn = (message: string, severity: string) => void;
5
+ /** Source de clé configurée (`config.jwt.keystore`). */
6
+ interface KeystoreSource {
7
+ /** JWK Set (clés privées) injecté depuis l'env — source `env` (prod). */
8
+ readonly keySetJson?: string;
9
+ /** Dossier de persistance `keyset.json` — source `fichier` (opt-in dev/VPS). */
10
+ readonly dir?: string;
11
+ }
12
+ /**
13
+ * Keystore Ed25519 — implémentation de référence d'{@link IJwtKeystore}.
14
+ *
15
+ * Résout la clé de signature selon une **priorité** (jamais d'auto-génération en
16
+ * clair « par défaut » en prod) :
17
+ * 1. **env** — `config.jwt.keystore.keySetJson` (JWK Set injecté par l'app depuis
18
+ * son catalogue d'env) : prod cloud, secret géré hors-app, même clé sur tous
19
+ * les pods.
20
+ * 2. **fichier** — `config.jwt.keystore.dir/keyset.json` (écrit en mode 600,
21
+ * généré si absent) : opt-in dev/VPS mono-machine. Le mode effectif est
22
+ * **constaté** après coup : un système de fichiers qui n'applique pas les
23
+ * permissions POSIX (NTFS, FAT, NFS sans mapping) déclenche un **warning**
24
+ * plutôt qu'une garantie silencieusement fausse.
25
+ * 3. **mémoire** — aucune source → clé éphémère générée au 1ᵉʳ usage + **warning**
26
+ * (perdue au redémarrage = refresh invalidés, incohérente en cluster).
27
+ *
28
+ * jose est importé **paresseusement** (dep lourde — règle perf P6) au premier
29
+ * usage ; le boot ne paie rien si le JWT n'est jamais sollicité. Le chargement
30
+ * est mémoïsé (une seule résolution concurrente).
31
+ *
32
+ * @remarks Race au 1ᵉʳ boot d'un **cluster** sans clé pré-provisionnée : deux
33
+ * workers peuvent générer puis écrire des clés différentes (le dernier `rename`
34
+ * gagne). En prod, provisionner la clé hors-bande (`keySetJson`/SecretProvider
35
+ * P16) élimine ce cas — c'est précisément la source recommandée.
36
+ */
37
+ export declare class JwtKeystore implements IJwtKeystore {
38
+ #private;
39
+ constructor(source: KeystoreSource, log: LogFn);
40
+ getSigningKey(): Promise<IJwtSigningKey>;
41
+ getPublicJWKS(): Promise<JSONWebKeySet>;
42
+ }
43
+ export default JwtKeystore;
@@ -0,0 +1,66 @@
1
+ import type { IPage } from "nodefony";
2
+ import type { IAccessTokenRecord, ITokenListQuery, ITokenStore, ITokenUsage, TokenRevokeReason } from "../../contracts/ITokenStore.js";
3
+ /**
4
+ * Filtre un record contre une requête de listing — prédicat de RÉFÉRENCE du
5
+ * contrat, réutilisé par les implémentations qui évaluent en mémoire.
6
+ *
7
+ * @param now - instant de référence pour l'état du jeton (`status`). Requis :
8
+ * « expiré » n'a pas de sens sans une horloge, et la lire ici ferait dépendre
9
+ * le résultat du moment du test plutôt que de la donnée.
10
+ */
11
+ export declare function matchesTokenQuery(record: IAccessTokenRecord, query: ITokenListQuery, now: number): boolean;
12
+ /**
13
+ * Instantané sérialisable de l'état d'un store en mémoire — base de la
14
+ * persistance fichier ({@link MemoryTokenStore.snapshot}/`restore`) et de
15
+ * l'inspection. Les index dérivés (par hash/famille/sujet) ne sont PAS
16
+ * sérialisés : ils sont reconstruits depuis `records` au `restore`.
17
+ */
18
+ export interface TokenStoreSnapshot {
19
+ records: IAccessTokenRecord[];
20
+ deniedJti: Array<[string, number]>;
21
+ invalidBefore: Array<[string, number]>;
22
+ }
23
+ /**
24
+ * Store de jetons **en mémoire** — implémentation de référence d'{@link ITokenStore}.
25
+ *
26
+ * 0 dépendance, idéale pour le développement mono-process et les **tests**. NON
27
+ * partagée entre process (pas de cluster) et **volatile** (tout est perdu au
28
+ * redémarrage) → en production multi-process, utiliser un adapter ORM ou Redis.
29
+ *
30
+ * Perf/mémoire : les `Map` n'existent que si le store est instancié (JWT activé),
31
+ * jamais sur le hot path par requête. La denylist `jti` est bornée par un
32
+ * **balayage amorti** (purge des entrées expirées tous les 256 ajouts) doublé
33
+ * d'une expiration paresseuse à la lecture — pas de minuterie, pas de fuite.
34
+ *
35
+ * Horloge injectable (`now`) pour des tests déterministes (pattern `LoginThrottler`).
36
+ */
37
+ export declare class MemoryTokenStore implements ITokenStore {
38
+ #private;
39
+ /**
40
+ * {@inheritDoc ITokenStore.sortableFields}
41
+ *
42
+ * Le store mémoire porte l'enregistrement complet : il sait donc trier tout le
43
+ * vocabulaire public, sans réduction de capacité.
44
+ */
45
+ readonly sortableFields: readonly ["createdAt", "name", "subjectId", "id"];
46
+ constructor(now?: () => number, retentionRevokedMs?: number);
47
+ put(record: IAccessTokenRecord): Promise<void>;
48
+ findById(id: string): Promise<IAccessTokenRecord | null>;
49
+ findByHash(secretHash: string): Promise<IAccessTokenRecord | null>;
50
+ findBySubject(subjectId: string): Promise<IAccessTokenRecord[]>;
51
+ listAll(): Promise<IAccessTokenRecord[]>;
52
+ listPage(query: ITokenListQuery): Promise<IPage<IAccessTokenRecord>>;
53
+ countTokens(query: ITokenListQuery): Promise<number>;
54
+ markUsed(id: string, usage: ITokenUsage): Promise<void>;
55
+ revoke(id: string, reason: TokenRevokeReason): Promise<void>;
56
+ revokeFamily(family: string, reason: TokenRevokeReason): Promise<void>;
57
+ denyJti(jti: string, expiresAt: number): Promise<void>;
58
+ isJtiDenied(jti: string): Promise<boolean>;
59
+ revokeAllForSubject(subjectId: string, invalidBefore: number): Promise<void>;
60
+ getInvalidBefore(subjectId: string): Promise<number | null>;
61
+ gc(now?: number): Promise<number>;
62
+ /** Instantané sérialisable de l'état courant (records + denylist + seuils). */
63
+ snapshot(): TokenStoreSnapshot;
64
+ /** Remplace l'état par celui d'un instantané (reconstruit les index dérivés). */
65
+ restore(snapshot: TokenStoreSnapshot): void;
66
+ }
@@ -0,0 +1,149 @@
1
+ import type * as Jose from "jose";
2
+ import { type IAccessPrincipal } from "nodefony";
3
+ /**
4
+ * Un émetteur en qui l'application accepte de faire confiance.
5
+ *
6
+ * Le fait qu'il n'y ait pas de valeur par défaut pour `issuer` est le cœur du
7
+ * dispositif : **la liste des émetteurs est fermée et vient de la
8
+ * configuration**, jamais d'un jeton. Le `iss` présenté ne sert qu'à choisir
9
+ * DANS cette liste — il ne peut donc pas désigner un serveur que l'application
10
+ * n'a pas nommé, et aucune requête sortante ne peut être provoquée par un
11
+ * appelant anonyme vers une URL de son choix.
12
+ */
13
+ export interface ITrustedIssuer {
14
+ /** Identifiant canonique de l'émetteur (`iss` attendu dans les jetons). */
15
+ issuer: string;
16
+ /**
17
+ * Jeu de clés, quand on ne veut pas de découverte.
18
+ *
19
+ * Utile pour un émetteur qui ne publie pas de métadonnées, et pour supprimer
20
+ * une requête au démarrage à froid. Déclaré, il fait autorité : rien n'est
21
+ * découvert.
22
+ */
23
+ jwksUri?: string;
24
+ /**
25
+ * Jeu de clés fourni LOCALEMENT — aucune requête, aucune découverte.
26
+ *
27
+ * ⭐ **Le cas qui l'exige : l'émetteur, c'est CETTE application.** Elle signe
28
+ * ses propres jetons, elle a donc déjà les clés publiques en mémoire. Aller
29
+ * les relire par HTTP chez elle-même ajoute à une opération purement locale
30
+ * une dépendance au réseau, au DNS et à TLS — et c'est exactement là que ça
31
+ * casse : en développement, l'application se sert un certificat que le
32
+ * magasin d'autorités de Node ne connaît pas, si bien que la vérification
33
+ * échouait en `SELF_SIGNED_CERT_IN_CHAIN` alors que le jeton était parfait,
34
+ * que la clé était à portée de main, et que `curl` joignait la même URL sans
35
+ * broncher. En production, le même aller-retour ferait dépendre
36
+ * l'authentification de l'entrée réseau du pod.
37
+ *
38
+ * Appelé à CHAQUE résolution, jamais mémoïsé : une rotation de clés locale
39
+ * est alors prise en compte sans redémarrage, et le coût — analyser un jeu de
40
+ * une ou deux clés — est sans commune mesure avec la vérification de
41
+ * signature qui suit.
42
+ */
43
+ localJwks?: () => Promise<Jose.JSONWebKeySet>;
44
+ /**
45
+ * Algorithmes de signature acceptés — **allowlist côté serveur**.
46
+ *
47
+ * RFC 8725 §3.1 : l'algorithme ne se déduit JAMAIS de l'en-tête du jeton.
48
+ * Tous asymétriques, et c'est structurel : les clés viennent d'un jeu PUBLIC,
49
+ * donc accepter un algorithme à secret partagé (`HS*`) laisserait un attaquant
50
+ * signer avec la clé publique de l'émetteur, que tout le monde peut lire.
51
+ */
52
+ algorithms: readonly string[];
53
+ /**
54
+ * Valeur exigée de l'en-tête `typ` (RFC 9068 : `at+jwt`), ou rien.
55
+ *
56
+ * Non exigé par défaut : le parc réel est très inégal sur ce point, et un
57
+ * défaut strict serait désactivé en bloc à la première intégration plutôt que
58
+ * réglé finement. La séparation entre jetons est déjà assurée par l'audience,
59
+ * qui, elle, n'est pas facultative.
60
+ */
61
+ typ?: string;
62
+ /** Claims dont la PRÉSENCE est exigée, en plus de `iss`/`aud`/`sub`. */
63
+ requiredClaims?: readonly string[];
64
+ }
65
+ /** Réglages du vérificateur — au-delà de la liste des émetteurs. */
66
+ export interface IRemoteJwtVerifierOptions {
67
+ /** Les émetteurs de confiance. Vide = le vérificateur ne sert à rien. */
68
+ issuers: readonly ITrustedIssuer[];
69
+ /** Délai maximal d'une requête vers un émetteur (ms). */
70
+ timeoutMs?: number;
71
+ /** Fenêtre pendant laquelle on ne redemande PAS le jeu de clés (ms). */
72
+ cooldownMs?: number;
73
+ /** Âge maximal du jeu de clés en cache avant rafraîchissement (ms). */
74
+ cacheMaxAgeMs?: number;
75
+ /** Tolérance d'horloge sur `exp`/`nbf` (secondes). */
76
+ clockToleranceS?: number;
77
+ /**
78
+ * Implémentation de `fetch` — le seul moyen d'éprouver ce code SANS réseau.
79
+ *
80
+ * C'est ce qui permet aux tests de jouer une rotation de clés, un émetteur
81
+ * qui ment sur son identité ou un délai dépassé, de façon déterministe et
82
+ * sans démarrer quoi que ce soit.
83
+ */
84
+ fetch?: typeof globalThis.fetch;
85
+ /** Journal d'audit — reçoit la cause FINE, que le client ne voit jamais. */
86
+ log?: (message: string) => void;
87
+ }
88
+ /**
89
+ * Vérificateur de jetons d'accès émis par un **serveur d'autorisation tiers**.
90
+ *
91
+ * C'est la pièce qui manquait pour que le rôle *serveur de ressource* du cœur
92
+ * (`nodefony/src/oauth/`) soit autre chose qu'un refus poli : il sait publier ce
93
+ * qu'il protège et dire où prendre un jeton, mais rien, jusqu'ici, ne savait
94
+ * LIRE ce jeton. `JwtAuthenticator` ne vérifie que les jetons émis par
95
+ * Nodefony lui-même (jeu de clés local) ; ici, les clés appartiennent à
96
+ * quelqu'un d'autre, arrivent par le réseau et tournent sans prévenir.
97
+ *
98
+ * ## Ce qui vaut garantie
99
+ *
100
+ * - **L'audience est obligatoire et vient de l'APPELANT** — jamais du jeton. Un
101
+ * jeton parfaitement valide, émis par un émetteur de confiance, pour un AUTRE
102
+ * service, est refusé (RFC 8707 §2). C'est la seule chose qui empêche le
103
+ * rejeu d'un jeton légitime d'une ressource vers une autre.
104
+ * - **L'algorithme est imposé par la configuration** (RFC 8725 §3.1), jamais lu
105
+ * dans l'en-tête ; `alg: none` n'existe pas pour cette API.
106
+ * - **Les clés viennent du `jwks_uri` de l'émetteur**, jamais d'un `jku` ou
107
+ * d'un `jwk` porté par le jeton (§3.5) — sans quoi un attaquant fournirait
108
+ * la clé qui valide sa propre signature.
109
+ * - **La liste des émetteurs est fermée** : un `iss` inconnu est refusé avant
110
+ * toute requête sortante.
111
+ *
112
+ * ## Ce que cette classe ne fait pas
113
+ *
114
+ * Elle n'établit pas d'utilisateur applicatif : elle rend un sujet et des
115
+ * scopes. Rattacher ce sujet à un compte local (approvisionnement à la volée,
116
+ * comptes de service) est une décision d'application, pas de protocole — et
117
+ * l'entremêler ici rendrait impossible d'accepter un appelant purement machine,
118
+ * qui est précisément le cas d'usage.
119
+ *
120
+ * @see references/rfc/ietf/rfc8707.txt — l'audience, qui LIE un jeton à CE service
121
+ */
122
+ export declare class RemoteJwtVerifier {
123
+ #private;
124
+ /**
125
+ * @param options - émetteurs de confiance et réglages réseau
126
+ * @throws Error si un émetteur est invalide, dupliqué, ou déclare un
127
+ * algorithme à secret partagé
128
+ */
129
+ constructor(options: IRemoteJwtVerifierOptions);
130
+ /** Nombre d'émetteurs de confiance — pour l'introspection et les journaux. */
131
+ get size(): number;
132
+ /**
133
+ * Vérifie un jeton porté, pour UNE ressource donnée.
134
+ *
135
+ * Conforme au contrat `IAccessTokenVerifier` du cœur : un refus est un `null`,
136
+ * jamais une exception. Les exceptions sont réservées aux pannes — un
137
+ * émetteur injoignable n'est pas un jeton invalide.
138
+ *
139
+ * @param token - le jeton brut, tel que présenté
140
+ * @param audience - URI canonique de la ressource visée ; le jeton DOIT la
141
+ * porter dans `aud`
142
+ * @returns le principal établi, ou `null` si le jeton est refusé
143
+ * @throws Error si l'émetteur ne peut pas être joint ou publie un jeu de clés
144
+ * inutilisable — la porte doit alors refuser de servir, pas répondre
145
+ * « jeton invalide »
146
+ */
147
+ verify(token: string, audience: string): Promise<IAccessPrincipal | null>;
148
+ }
149
+ export default RemoteJwtVerifier;
@@ -0,0 +1,41 @@
1
+ import type { IUser } from "@nodefony/user";
2
+ import type { IToken } from "../../contracts/IToken.js";
3
+ /**
4
+ * Jeton porteur d'un utilisateur réel — produit par les authenticators à
5
+ * credential (`userpassword`, puis `session`/`jwt`...).
6
+ *
7
+ * Cycle en deux états, UN SEUL objet alloué par tentative (cold path login) :
8
+ * 1. `createToken()` → non authentifié : porte le credential brut extrait de la
9
+ * requête, `getUser()` rend l'anonyme (jamais `null`, Zero Trust).
10
+ * 2. `authenticate()` réussit → {@link promote} : l'utilisateur vérifié est posé
11
+ * et le credential est **effacé** (anti-fuite : un mot de passe ne doit
12
+ * survivre ni en mémoire ni dans un heap dump/log).
13
+ *
14
+ * Les attributs (claims, providerId...) sont lazy — `null` tant que rien n'est posé.
15
+ */
16
+ export declare class UserToken implements IToken {
17
+ #private;
18
+ readonly type: string;
19
+ /**
20
+ * @param type - type du token (`"userpassword"`, `"session"`, `"jwt"`...).
21
+ * @param credentials - credential brut extrait de la requête (vidé au succès).
22
+ */
23
+ constructor(type: string, credentials?: unknown);
24
+ /**
25
+ * Marque le jeton authentifié : pose l'utilisateur vérifié et EFFACE le
26
+ * credential. Appelé uniquement par l'authenticator au succès.
27
+ *
28
+ * @param user - utilisateur vérifié par la source d'identité.
29
+ * @returns le jeton lui-même (chaînage).
30
+ */
31
+ promote(user: IUser): this;
32
+ getUser(): IUser;
33
+ getUserIdentifier(): string;
34
+ isAuthenticated(): boolean;
35
+ getRoles(): string[];
36
+ getCredentials(): unknown;
37
+ getScopes(): string[];
38
+ getAttribute<T = unknown>(key: string): T | undefined;
39
+ setAttribute(key: string, value: unknown): void;
40
+ }
41
+ export default UserToken;
@@ -0,0 +1,28 @@
1
+ import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
2
+ /**
3
+ * Paramètres JWT **résolus** partagés par l'émetteur ({@link TokenService}) et le
4
+ * vérificateur ({@link JwtAuthenticator}) — garantit que `iss`/`aud` posés à la
5
+ * signature sont EXACTEMENT ceux exigés à la vérification (une divergence = tout
6
+ * rejeté). Fonction pure (pas d'état, pas de kernel) → les deux côtés obtiennent
7
+ * la même valeur sans la partager.
8
+ */
9
+ export interface IJwtRuntime {
10
+ /** Émetteur (`iss`). */
11
+ readonly issuer: string;
12
+ /** Audiences acceptées au verify ; la première sert d'`aud` à l'émission. */
13
+ readonly audiences: string[];
14
+ /** TTL access token (s). */
15
+ readonly accessTtlS: number;
16
+ /** TTL refresh token (s). */
17
+ readonly refreshTtlS: number;
18
+ /** Rotation du refresh à chaque usage (OWASP / RFC 9700). */
19
+ readonly rotateRefresh: boolean;
20
+ /** Algorithme — `"EdDSA"` (Ed25519). RS256 = slot non câblé en J4a. */
21
+ readonly alg: "EdDSA";
22
+ }
23
+ /**
24
+ * Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
25
+ * `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
26
+ * propre audience (`[issuer]`).
27
+ */
28
+ export declare function resolveJwtRuntime(jwt: ISecurityConfig["jwt"]): IJwtRuntime;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Écrire un SECRET sur disque — la seule implémentation du dépôt.
3
+ *
4
+ * ## Les trois règles, et ce que chacune évite
5
+ *
6
+ * 1. **Ne jamais tester la présence avant de lire.** `existsSync(f) ? read(f)
7
+ * : ""` ouvre une fenêtre entre le test et l'usage : le fichier peut
8
+ * disparaître, ou devenir un lien vers ailleurs. La forme juste est de lire
9
+ * et de traiter `ENOENT` — le système de fichiers répond en une opération ce
10
+ * que deux appels ne peuvent pas garantir.
11
+ * 2. **Écrire en 0600, atomiquement.** Un secret créé au masque par défaut est
12
+ * lisible par tous les comptes de la machine, et rien ne le signale. Le
13
+ * couple fichier temporaire + `rename` évite en plus qu'un lecteur tombe sur
14
+ * un fichier à demi écrit.
15
+ * 3. **CONSTATER le mode obtenu.** Le mode demandé est une intention, pas une
16
+ * garantie : NTFS l'ignore, comme un montage FAT/exFAT ou NFS sans mapping
17
+ * d'identité. Une capacité se constate, elle ne se déduit pas de
18
+ * `process.platform` — et si la restriction n'a pas pris, il faut le DIRE
19
+ * plutôt que laisser croire à une protection.
20
+ *
21
+ * ## Pourquoi les deux formes, synchrone et asynchrone
22
+ *
23
+ * Le runtime persiste ses clés dans du code asynchrone ; une commande de CLI
24
+ * écrit un jeton dans un flot synchrone, où introduire une promesse
25
+ * changerait l'ordre des messages affichés. Les deux formes appliquent le même
26
+ * raisonnement, écrit ici une seule fois — deux copies divergeraient, et l'on
27
+ * sait exactement comment : l'une porterait le mode 0600, l'autre non.
28
+ *
29
+ * @module
30
+ */
31
+ /** Mode attendu d'un fichier qui porte un secret : lisible par son seul propriétaire. */
32
+ export declare const MODE_SECRET = 384;
33
+ /**
34
+ * Le contenu du fichier, ou `null` s'il n'existe pas.
35
+ *
36
+ * @throws Toute erreur autre qu'`ENOENT` — un fichier illisible pour cause de
37
+ * droits n'est PAS un fichier absent, et le confondre ferait écraser un
38
+ * secret existant par un fichier neuf.
39
+ */
40
+ export declare function readIfPresent(file: string): Promise<string | null>;
41
+ /** Forme synchrone de {@link readIfPresent}. */
42
+ export declare function readIfPresentSync(file: string): string | null;
43
+ /**
44
+ * Le mode effectif du fichier n'est-il PAS restreint au propriétaire ?
45
+ *
46
+ * @returns `null` si le fichier a disparu ou n'est pas interrogeable (le chemin
47
+ * d'erreur normal parlera), sinon le mode effectif quand il diffère de
48
+ * 0600 — et `undefined` quand tout va bien.
49
+ */
50
+ export declare function modeNonRestreint(file: string): number | null | undefined;
51
+ /** Forme asynchrone de {@link modeNonRestreint}. */
52
+ export declare function modeNonRestreintAsync(file: string): Promise<number | null | undefined>;
53
+ /**
54
+ * La phrase à journaliser quand la restriction n'a PAS pris.
55
+ *
56
+ * Elle nomme la cause probable et ce qui reste à faire : un avertissement qui
57
+ * dit seulement « mode inattendu » se lit comme du bruit et finit ignoré.
58
+ */
59
+ export declare function messageNonRestreint(file: string, mode: number): string;
60
+ /**
61
+ * Écrit un secret : dossier créé, mode 0600, remplacement ATOMIQUE.
62
+ *
63
+ * Le mode est posé à la création du temporaire — et non après `rename` — pour
64
+ * qu'il n'existe à aucun instant un fichier au contenu secret et au masque par
65
+ * défaut. `chmod` est ensuite réappliqué sur la cible : un `rename` par-dessus
66
+ * un fichier EXISTANT conserve, sur certains systèmes, le mode de la cible.
67
+ */
68
+ export declare function writeSecret(file: string, content: string): Promise<void>;
69
+ /** Forme synchrone de {@link writeSecret}. */
70
+ export declare function writeSecretSync(file: string, content: string): void;
@@ -0,0 +1,20 @@
1
+ import type { ITokenListQuery } from "../../contracts/ITokenStore.js";
2
+ /**
3
+ * Fragment de critère **portable** exprimant l'état de vie d'un jeton — la
4
+ * traduction que les adapters SQL et Mongo partagent au lieu de la réécrire.
5
+ *
6
+ * Elle tient dans le `Criteria` d'orm-core depuis que celui-ci porte `$or` :
7
+ * « utilisable » signifie *sans échéance* **ou** *échéance à venir*, ce qu'aucune
8
+ * conjonction ne dit. Avant ça, chaque backend serait descendu à son SQL natif —
9
+ * trois écritures de la même règle, et la divergence pour seule perspective.
10
+ *
11
+ * Révoqué l'emporte sur expiré : les deux autres branches exigent donc
12
+ * explicitement `revokedAt IS NULL`. Sans cette précision, une clé révoquée
13
+ * **puis** échue compterait dans deux facettes, et la somme dépasserait le total.
14
+ *
15
+ * @param status - l'état demandé, ou `undefined` pour ne pas filtrer.
16
+ * @param now - instant de référence (injecté : un compteur ne doit pas dépendre
17
+ * du moment où le test tourne).
18
+ * @returns le fragment à fusionner dans le critère, vide si aucun filtre.
19
+ */
20
+ export declare function tokenStatusCriteria(status: ITokenListQuery["status"], now: number): Record<string, unknown>;
@@ -0,0 +1,76 @@
1
+ import type { FacetCounts } from "nodefony";
2
+ /**
3
+ * **Le vocabulaire de filtre des jetons**, en noms PUBLICS — ceux qu'un client
4
+ * écrit dans l'URL (`?status=revoked`), jamais des noms de colonne.
5
+ *
6
+ * Frère de `TOKEN_SORTABLE_FIELDS`, et posé pour la même raison : le vocabulaire
7
+ * appartient au propriétaire du contrat (`@nodefony/security`), la mécanique de
8
+ * lecture au cœur (`parseFilters`).
9
+ *
10
+ * **La différence avec le tri est structurelle.** Un tri est une CAPACITÉ de
11
+ * backend — Redis ne sait pas trier, donc `sortableFields` se déclare par store.
12
+ * Un filtre listé ici est une OBLIGATION de tous les backends de jetons : il est
13
+ * inscrit dans {@link ITokenListQuery}, et le store mémoire, SQL, Mongo comme
14
+ * Redis l'honorent chacun à sa façon (`WHERE` indexé, prédicat, filtre inline de
15
+ * batch `SCAN`). Le déclarer par store laisserait croire qu'il est facultatif.
16
+ *
17
+ * **Ce qui n'y est PAS, et pourquoi** : `kind`. Il existe bien au contrat, mais
18
+ * l'endpoint d'administration des clés d'API passe par `listPagePat`, qui impose
19
+ * `kind: "pat"` (`service/apiKeys.ts:210`). L'exposer donnerait un filtre que le
20
+ * service écrase en silence — la faute même que ce chantier corrige.
21
+ */
22
+ export declare const TOKEN_FILTERS: {
23
+ /** Restreint à un porteur (colonne indexée dans tous les backends SQL). */
24
+ readonly subjectId: "string";
25
+ /**
26
+ * État de vie de la clé — la liste fermée vaut allowlist.
27
+ *
28
+ * Remplace l'ancien `revoked: "boolean"`, qui ne distinguait pas une clé
29
+ * ACTIVE d'une clé ARRIVÉE À ÉCHÉANCE : les deux étaient « non révoquées »,
30
+ * alors que la première ouvre l'accès et la seconde ne l'ouvre plus. La
31
+ * console affichait ces deux populations dans des cartes séparées sans
32
+ * pouvoir les demander au serveur.
33
+ */
34
+ readonly status: readonly ["active", "expired", "revoked"];
35
+ };
36
+ /**
37
+ * **Les facettes des jetons** — les questions fermées posées à la collection
38
+ * ENTIÈRE pour les cartes de tête.
39
+ *
40
+ * Contrairement aux webhooks, les trois états **partitionnent** : un jeton est
41
+ * dans exactement une case. On les compte tout de même une par une, sans jamais
42
+ * soustraire — une partition est une propriété du domaine d'aujourd'hui, pas une
43
+ * garantie du code, et un quatrième état la briserait en silence.
44
+ */
45
+ export declare const TOKEN_FACETS: {
46
+ /** Toutes les clés, quel que soit leur état. */
47
+ readonly total: {};
48
+ /** Utilisables : ni révoquées, ni arrivées à échéance. */
49
+ readonly active: {
50
+ readonly status: "active";
51
+ };
52
+ /** Arrivées à échéance sans avoir été révoquées. */
53
+ readonly expired: {
54
+ readonly status: "expired";
55
+ };
56
+ /** Révoquées par un administrateur. */
57
+ readonly revoked: {
58
+ readonly status: "revoked";
59
+ };
60
+ };
61
+ /** Les compteurs rendus par `GET /nodefony/security/api/apikeys/stats`. */
62
+ export type ITokenCounts = FacetCounts<typeof TOKEN_FACETS>;
63
+ /**
64
+ * Ce que l'endpoint de COMPTEURS accepte de filtrer — `TOKEN_FILTERS` **moins**
65
+ * les champs que les facettes décomposent.
66
+ *
67
+ * `status` en est retiré : le demander à un endpoint dont les cartes SONT les
68
+ * états produirait une réponse qui se contredit — un total suivant le filtre, et
69
+ * chaque facette l'écrasant par le sien. Le refuser dit au client ce qui se
70
+ * passe ; l'accepter lui montrerait « 5 clés, dont 538 révoquées ».
71
+ *
72
+ * Un test verrouille l'accord entre cette liste et {@link TOKEN_FACETS}.
73
+ */
74
+ export declare const TOKEN_STATS_FILTERS: {
75
+ readonly subjectId: "string";
76
+ };
@@ -0,0 +1,33 @@
1
+ import type { IPageQuery } from "nodefony";
2
+ /**
3
+ * **Le vocabulaire de tri des jetons**, en noms PUBLICS — ceux qu'un client écrit
4
+ * dans l'URL (`?order=createdAt:DESC`), jamais des noms de colonne.
5
+ *
6
+ * Il vit ici, chez le propriétaire du contrat (`@nodefony/security`), et non dans
7
+ * chaque backend : c'est ce qui garantit qu'une console de clés d'API offre le
8
+ * même tri, que l'application tourne sur mémoire, SQL ou Mongo. Un store dont le
9
+ * schéma nomme un champ autrement traduit **chez lui** — cf
10
+ * {@link translateTokenOrderMongo}.
11
+ *
12
+ * - `createdAt` — date d'émission, l'axe naturel d'une console de clés ;
13
+ * - `name` — le libellé humain, ce qu'on lit dans la colonne de gauche ;
14
+ * - `subjectId` — regroupe les clés d'un même porteur (vue d'administration) ;
15
+ * - `id` — identifiant public, utile surtout en départage.
16
+ *
17
+ * **Ce qui n'y est PAS, et pourquoi** : `lastUsedAt`, `expiresAt` et `revokedAt`
18
+ * sont *nullables*, et le placement des valeurs absentes n'est pas le même d'un
19
+ * moteur à l'autre — PostgreSQL range les `NULL` en tête d'un tri `DESC`, SQLite
20
+ * et MySQL en queue, et le tri en mémoire (`compareByOrder`) les met en queue
21
+ * dans les deux sens. Les déclarer offrirait donc un tri dont l'ordre
22
+ * dépendrait de la base configurée, ce qui est exactement ce que ce vocabulaire
23
+ * existe pour empêcher. Ils s'ouvriront quand la normalisation « absents en
24
+ * queue » sera portée dans le helper de pagination, pas avant.
25
+ */
26
+ export declare const TOKEN_SORTABLE_FIELDS: readonly ["createdAt", "name", "subjectId", "id"];
27
+ /**
28
+ * Ordre contractuel appliqué quand le client n'en demande aucun : les clés les
29
+ * plus récentes d'abord, départagées par identifiant pour rester **déterministe**
30
+ * à horodatage égal (sans quoi une pagination offset peut sauter ou répéter une
31
+ * ligne entre deux pages).
32
+ */
33
+ export declare const TOKEN_DEFAULT_ORDER: NonNullable<IPageQuery["order"]>;
@@ -0,0 +1,38 @@
1
+ import type { TokenStatus } from "../../contracts/ITokenStore.js";
2
+ /**
3
+ * Ce qu'il faut d'un jeton pour en déduire l'état — deux horodatages, rien de
4
+ * plus. Volontairement structural : le store mémoire, le batch Redis et un test
5
+ * s'en servent sans partager de type d'enregistrement.
6
+ */
7
+ export interface ITokenLifetime {
8
+ /** Instant de révocation, ou `null` si jamais révoqué. */
9
+ readonly revokedAt: number | null;
10
+ /** Échéance, ou `null` pour un jeton sans expiration. */
11
+ readonly expiresAt: number | null;
12
+ }
13
+ /**
14
+ * **La** définition de l'état d'un jeton — un seul exemplaire, pour les backends
15
+ * qui évaluent en mémoire (store mémoire, filtrage inline d'un batch `SCAN`).
16
+ *
17
+ * L'ordre d'évaluation est significatif : **révoqué l'emporte sur expiré**. Une
18
+ * clé révoquée puis arrivée à échéance reste « révoquée » — c'est l'acte
19
+ * d'administration qui décrit ce qui s'est passé, pas l'écoulement du temps.
20
+ *
21
+ * Les backends SQL et Mongo n'appellent pas cette fonction (ils traduisent la
22
+ * condition dans leur langage, sinon il faudrait rapatrier la collection pour la
23
+ * filtrer) : c'est le banc de contrat partagé qui garantit qu'ils disent la même
24
+ * chose qu'elle.
25
+ *
26
+ * @param token - les deux horodatages du jeton.
27
+ * @param now - l'instant de référence (injecté : les tests ne dépendent pas de
28
+ * l'horloge réelle, et un store porte déjà la sienne).
29
+ */
30
+ export declare function tokenStatusOf(token: ITokenLifetime, now: number): TokenStatus;
31
+ /**
32
+ * `true` si le jeton correspond au filtre d'état demandé (`undefined` = tous).
33
+ *
34
+ * @param token - les deux horodatages du jeton.
35
+ * @param status - l'état demandé, ou `undefined` pour ne pas filtrer.
36
+ * @param now - l'instant de référence.
37
+ */
38
+ export declare function matchesTokenStatus(token: ITokenLifetime, status: TokenStatus | undefined, now: number): boolean;
@@ -0,0 +1,38 @@
1
+ import type { Container } from "nodefony";
2
+ import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
3
+ import type { ITokenStore } from "../../contracts/ITokenStore.js";
4
+ /**
5
+ * Registre de **fabriques de stores de jetons** — résout le nom configuré
6
+ * (`security.tokenStore`) vers une instance d'{@link ITokenStore}, SANS coupler
7
+ * le cœur à un backend en dur.
8
+ *
9
+ * Pourquoi : le store est pluggable par contrat (mémoire/fichier/ORM/Redis) ;
10
+ * un `if (name === "redis")` trahirait cette promesse. Les builtins sans
11
+ * dépendance (`memory`) s'enregistrent au chargement du module ; les adapters
12
+ * lourds (`drizzle`, `mongoose`, `redis`) s'enregistrent depuis LEUR module
13
+ * (inversion de dépendance : ils importent `import type { ITokenStore }`, effacé
14
+ * à la compilation). Convention-frère : `authenticatorRegistry`, `ormRegistry`,
15
+ * `SessionsService.registerStorage`.
16
+ */
17
+ /**
18
+ * Contexte passé à une fabrique de store : de quoi se construire (résolutions
19
+ * coûteuses en lazy à l'intérieur de l'instance).
20
+ */
21
+ export interface ITokenStoreFactoryContext {
22
+ /** Container DI — résolution de services (ORM, redis...). */
23
+ readonly container: Container;
24
+ /** Config sécurité validée + gelée. */
25
+ readonly config: ISecurityConfig;
26
+ }
27
+ /** Fabrique d'un store de jetons pour un nom donné. */
28
+ export type TokenStoreFactory = (ctx: ITokenStoreFactoryContext) => ITokenStore;
29
+ /**
30
+ * Enregistre (ou remplace) la fabrique d'un store de jetons. Appelée par le
31
+ * builtin `memory` au chargement, et par les adapters (drizzle/mongoose/redis)
32
+ * pour les leurs.
33
+ */
34
+ export declare function registerTokenStore(name: string, factory: TokenStoreFactory): void;
35
+ /** Fabrique d'un store par nom, ou `undefined` si inconnu. */
36
+ export declare function getTokenStoreFactory(name: string): TokenStoreFactory | undefined;
37
+ /** Noms enregistrés (validation boot, introspection Studio, tests). */
38
+ export declare function listTokenStores(): string[];
@@ -0,0 +1,43 @@
1
+ import type { IPage } from "nodefony";
2
+ import type { ITotpSecret } from "../../contracts/ITotpSecret.js";
3
+ import type { ITotpEnrollmentSummary, ITotpListQuery, ITotpSecretStore, TotpSecretUpdate } from "../../contracts/ITotpSecretStore.js";
4
+ /**
5
+ * Projette un secret en vue d'introspection — **le seul endroit** où l'on
6
+ * décide ce qui sort d'un store TOTP en mémoire. `secretEnc` et les condensats
7
+ * des codes de récupération n'y figurent pas : seul leur NOMBRE est exposé.
8
+ *
9
+ * @param secret - le secret stocké.
10
+ * @returns la vue publique de l'enrôlement.
11
+ */
12
+ export declare function toTotpEnrollment(secret: ITotpSecret): ITotpEnrollmentSummary;
13
+ /** Applique les filtres d'{@link ITotpListQuery} — sémantique de RÉFÉRENCE. */
14
+ export declare function matchesTotpQuery(secret: ITotpSecret, query: ITotpListQuery): boolean;
15
+ /** Instantané sérialisable de l'état — base de la persistance fichier. */
16
+ export interface TotpStoreSnapshot {
17
+ secrets: ITotpSecret[];
18
+ }
19
+ /**
20
+ * Store de secrets TOTP **en mémoire** — implémentation de référence
21
+ * d'{@link ITotpSecretStore}. Clé = `userId` (un secret par utilisateur).
22
+ *
23
+ * 0 dépendance, idéale pour le développement mono-process et les **tests**. NON
24
+ * partagée entre process et **volatile** → en production multi-process, utiliser
25
+ * un adapter ORM ou Redis. Perf : la `Map` n'existe que si le store est instancié
26
+ * (2FA activé), jamais sur le hot path par requête.
27
+ */
28
+ export declare class MemoryTotpSecretStore implements ITotpSecretStore {
29
+ #private;
30
+ findByUser(userId: string): Promise<ITotpSecret | null>;
31
+ save(secret: ITotpSecret): Promise<void>;
32
+ update(userId: string, patch: TotpSecretUpdate): Promise<void>;
33
+ delete(userId: string): Promise<void>;
34
+ /** {@inheritDoc ITotpSecretStore.listPage} */
35
+ listPage(query: ITotpListQuery): Promise<IPage<ITotpEnrollmentSummary>>;
36
+ /** {@inheritDoc ITotpSecretStore.countEnrollments} */
37
+ countEnrollments(query: ITotpListQuery): Promise<number>;
38
+ /** Instantané sérialisable de l'état courant (pour la persistance fichier). */
39
+ snapshot(): TotpStoreSnapshot;
40
+ /** Remplace l'état par celui d'un instantané. */
41
+ restore(snapshot: TotpStoreSnapshot): void;
42
+ }
43
+ export default MemoryTotpSecretStore;