@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,28 @@
1
+ import type { IToken } from "./IToken.js";
2
+ /**
3
+ * Service d'autorisation (niveau C de la sécurité, P6) — décide si un token a le
4
+ * droit d'effectuer une action (`attribute`) sur un sujet (`subject`).
5
+ *
6
+ * Distinct de l'authentification (firewall = *QUI es-tu*) : l'autorisation
7
+ * répond *PEUX-tu faire ça* via un jury de {@link IAccessVoter} agrégé en
8
+ * **affirmative + DENY veto** (un seul `DENY` bloque ; sinon un `GRANT` suffit ;
9
+ * silence total → `DENY`, Zero Trust).
10
+ *
11
+ * Consommé par les décorateurs (`@IsGranted`, J7) au hook `beforeResolve`, et
12
+ * par le verrou de frame WS (« 1 garde = N transports ») : la même décision sert
13
+ * REST et socket sans double implémentation.
14
+ */
15
+ export interface IAuthorizationService {
16
+ /**
17
+ * Le token a-t-il le droit demandé ? `false` = accès refusé (le caller
18
+ * répond 403).
19
+ *
20
+ * @param token - identité résolue par le firewall (jamais `null` — anonyme inclus).
21
+ * @param attribute - droit demandé : un rôle (`"ROLE_ADMIN"`), une permission
22
+ * (`"PERM_project_edit"`) ou un attribut métier libre (`"project.edit"`).
23
+ * @param subject - objet ciblé (entité, canal, path…) — passé aux voters
24
+ * contextuels (ownership, multi-tenant). `undefined` pour un droit global.
25
+ * @returns `true` si accordé, `false` sinon (DENY veto, ou aucun GRANT).
26
+ */
27
+ decide(token: IToken, attribute: string, subject?: unknown): Promise<boolean>;
28
+ }
@@ -0,0 +1,64 @@
1
+ import type { ContextType } from "@nodefony/http";
2
+ import type { IAuthenticator } from "./IAuthenticator.js";
3
+ import type { ISecuredArea } from "./ISecuredArea.js";
4
+ import type { CspFragment } from "../src/csp.js";
5
+ import type { IFirewallDescription, IRoleHierarchyDescription } from "./IFirewallDescription.js";
6
+ import type { IProtectedResourceInput } from "nodefony";
7
+ /**
8
+ * Orchestrateur de sécurité — branché dans le pipeline HTTP/WS de `@nodefony/http`.
9
+ *
10
+ * `isSecure()` (rapide, hot-path) ne fait QUE matcher la zone et poser
11
+ * `context.security`. `handleSecurity()` (lazy, seulement si la requête est dans
12
+ * une zone) exécute CORS → headers → authenticators → Zero Trust → CSRF.
13
+ */
14
+ export interface IFirewall {
15
+ /** Match rapide de zone (pose `context.security`). `true` si zone capturée. */
16
+ isSecure(context: ContextType): boolean;
17
+ /** Pipeline complet d'authentification de la zone. Rejette (401/403) ou résout. */
18
+ handleSecurity(context: ContextType): Promise<ContextType>;
19
+ /**
20
+ * Défense CSRF (Fetch Metadata + repli Origin) sur les méthodes state-changing.
21
+ * No-op sur les méthodes sûres et hors navigateur. Lève `CsrfError` (403) sinon.
22
+ */
23
+ enforceCsrf(context: ContextType): void;
24
+ /**
25
+ * Politique CORS : pose les en-têtes `Access-Control-*`. Retourne `204` pour un
26
+ * preflight `OPTIONS` (à court-circuiter), sinon `undefined`. No-op hors CORS.
27
+ */
28
+ handleCors(context: ContextType): number | undefined;
29
+ /**
30
+ * Pose les en-têtes de sécurité applicatifs (CSP, Referrer-Policy, COOP/COEP/
31
+ * CORP, Permissions-Policy) sur la réponse. No-op si désactivés / hors HTTP.
32
+ */
33
+ applySecurityHeaders(context: ContextType): void;
34
+ /**
35
+ * Déclare des directives CSP additionnelles pour un module (ex. `@nodefony/frontend`
36
+ * en dev : origines Vite + `'unsafe-eval'`). Fusionnées dans le CSP de base et
37
+ * recomposées (re-split nonce) au (dé)enregistrement — jamais par requête.
38
+ */
39
+ registerCspOrigins(moduleName: string, fragment: CspFragment): void;
40
+ /** Retire les directives CSP d'un module (ex. `stopDev` du frontend). */
41
+ unregisterCspOrigins(moduleName: string): void;
42
+ /** Enregistre un authenticator (appelé par chaque `*Authenticator` au boot). */
43
+ registerAuthenticator(authenticator: IAuthenticator): void;
44
+ /** Zone par nom, ou `undefined`. */
45
+ getArea(name: string): ISecuredArea | undefined;
46
+ /**
47
+ * Projection LECTURE SEULE de l'état RUNTIME (zones/authenticators/défenses)
48
+ * pour le data plane Studio — **secrets exclus** (présence, jamais valeur).
49
+ * Décrit ce qui TOURNE (pas la config brute). Source de vérité d'introspection.
50
+ */
51
+ describe(): IFirewallDescription;
52
+ /** Hiérarchie de rôles déclarée + résolution transitive (data plane Studio). */
53
+ describeRoleHierarchy(): IRoleHierarchyDescription;
54
+ /**
55
+ * Ressources protégées à publier en RFC 9728 — une par `area.resource`
56
+ * déclarée, avec les émetteurs de confiance comme serveurs d'autorisation.
57
+ *
58
+ * Consommée par `@nodefony/framework` (contrat STRUCTUREL, par nom de
59
+ * service : framework ne dépend jamais de security) pour monter les documents
60
+ * que le défi d'un `401` désigne. C'est la MÊME donnée des deux côtés — le
61
+ * pointeur et le document ne peuvent donc pas diverger.
62
+ */
63
+ publishedProtectedResources(): readonly IProtectedResourceInput[];
64
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Surface d'INTROSPECTION du firewall (data plane Studio P6.15) — projection
3
+ * LECTURE SEULE de l'état RUNTIME réel du {@link IFirewall} (zones montées,
4
+ * authenticators instanciés, défenses résolues), **secrets exclus par
5
+ * construction** (présence, jamais valeur — règle audit). Distincte de la config
6
+ * brute : on décrit ce qui TOURNE, pas ce que l'app a écrit (un nom
7
+ * d'authenticator en typo n'apparaît pas comme « monté »).
8
+ *
9
+ * Le front Studio en tient des **types miroir locaux** (frontière isomorphe :
10
+ * jamais d'import runtime `@nodefony/security` dans le bundle navigateur).
11
+ */
12
+ /** Une zone sécurisée telle que montée au boot (pattern compilé → source). */
13
+ export interface IFirewallZoneDescription {
14
+ /** Nom de la zone (`"nodefony-admin"`…). */
15
+ name: string;
16
+ /** Pattern d'URL (`RegExp.source`) capturé par la zone. */
17
+ pattern: string;
18
+ /** Zone protégée (Zero Trust) ; `false` = zone publique explicite. */
19
+ security: boolean;
20
+ /** `true` = chaque requête porte sa preuve (JWT/clé) ; `false` = session BFF possible. */
21
+ stateless: boolean;
22
+ /** Chaîne d'authenticators : `first` (le premier qui reconnaît) ou `all` (MFA). */
23
+ mode: "first" | "all";
24
+ /** Noms des authenticators exécutés par la zone (sémantique selon `mode`). */
25
+ authenticators: readonly string[];
26
+ /** Le pattern `anonymous` est-il listé (anonymat explicite autorisé) ? */
27
+ allowsAnonymous: boolean;
28
+ /** Domaine/vhost de la zone, ou `null` (tous domaines). */
29
+ host: string | null;
30
+ /** Zone valable aussi pour les frames WebSocket (api.request + subscribe). */
31
+ realtime: boolean;
32
+ }
33
+ /** Un authenticator : disponible (registre de fabriques) et/ou monté (≥1 zone). */
34
+ export interface IFirewallAuthenticatorDescription {
35
+ /** Nom logique (`anonymous`/`userpassword`/`session`/`jwt`/`apikey`…). */
36
+ name: string;
37
+ /** Référencé par ≥1 zone → instancié au boot (actif sur le pipeline). */
38
+ mounted: boolean;
39
+ /** Présent dans le registre de fabriques (utilisable en config). */
40
+ available: boolean;
41
+ /** Déclare un challenge `WWW-Authenticate` (RFC 7235). */
42
+ challenge: boolean;
43
+ }
44
+ /** Défenses transverses résolues (CSRF/CORS/headers/throttle) — sans secret. */
45
+ export interface IFirewallDefensesDescription {
46
+ csrf: {
47
+ enabled: boolean;
48
+ /** Défense primaire Fetch Metadata (`Sec-Fetch-Site`). */
49
+ fetchMetadata: boolean;
50
+ /** Repli Origin/Referer (vieux navigateurs). */
51
+ checkOrigin: boolean;
52
+ /** Politique stricte same-origin only (multi-tenant). */
53
+ strictSameSite: boolean;
54
+ /** Attribut cookie `SameSite`. */
55
+ sameSite: "Strict" | "Lax" | "None";
56
+ /** Alias d'origine légitimes (façades multi-domaine). */
57
+ trustedOrigins: readonly string[];
58
+ /** Synchronizer token `@CsrfProtect` armé (secret présent OU éphémère dev) — PRÉSENCE, jamais le secret. */
59
+ synchronizerToken: boolean;
60
+ };
61
+ cors: {
62
+ enabled: boolean;
63
+ origins: readonly string[];
64
+ credentials: boolean;
65
+ methods: readonly string[];
66
+ allowedHeaders: readonly string[];
67
+ exposedHeaders: readonly string[];
68
+ maxAgeS: number;
69
+ };
70
+ headers: {
71
+ enabled: boolean;
72
+ /** ⚙️ posé par @nodefony/http (transport) — reflété ici pour information. */
73
+ hsts: boolean;
74
+ hstsMaxAgeS: number;
75
+ /** Policy CSP (template `{{nonce}}`) — politique publique, pas un secret. */
76
+ csp: string;
77
+ cspNonces: boolean;
78
+ frameguard: "deny" | "sameorigin";
79
+ noSniff: boolean;
80
+ referrerPolicy: string;
81
+ coop?: string;
82
+ coep?: string;
83
+ corp?: string;
84
+ originAgentCluster?: boolean;
85
+ permissionsPolicy?: string;
86
+ };
87
+ rateLimit: {
88
+ enabled: boolean;
89
+ freeAttempts: number;
90
+ baseDelayS: number;
91
+ capDelayS: number;
92
+ };
93
+ }
94
+ /** Photo complète de l'état du firewall. */
95
+ export interface IFirewallDescription {
96
+ /** Config sécurité valide au boot (sinon le firewall est fail-closed). */
97
+ configValid: boolean;
98
+ /** Message d'erreur de config si fail-closed, sinon `null` (jamais de stack — Zero Trust). */
99
+ configError: string | null;
100
+ /** Zones montées, triées par spécificité (pattern le plus long d'abord). */
101
+ zones: IFirewallZoneDescription[];
102
+ /** Authenticators disponibles/montés (union registre ∪ instanciés). */
103
+ authenticators: IFirewallAuthenticatorDescription[];
104
+ /** Défenses résolues, ou `null` si la config est invalide. */
105
+ defenses: IFirewallDefensesDescription | null;
106
+ }
107
+ /** Un rôle déclaré + ses rôles hérités (résolution transitive du walker). */
108
+ export interface IRoleDescription {
109
+ /** Rôle déclaré (clé de la hiérarchie). */
110
+ role: string;
111
+ /** Tous les rôles atteignables transitivement (hors le rôle lui-même), triés. */
112
+ inherits: string[];
113
+ }
114
+ /** Hiérarchie de rôles : déclaration brute + résolution aplatie. */
115
+ export interface IRoleHierarchyDescription {
116
+ /** Déclaration directe (`{ ROLE_ADMIN: ["ROLE_USER"] }`). */
117
+ hierarchy: Record<string, string[]>;
118
+ /** Résolution transitive précalculée par le {@link RoleHierarchyWalker}. */
119
+ roles: IRoleDescription[];
120
+ }
@@ -0,0 +1,40 @@
1
+ import type { JSONWebKeySet } from "jose";
2
+ /**
3
+ * Clé active de signature résolue par le keystore — la clé privée Ed25519 plus
4
+ * son `kid` (à poser dans l'en-tête JWT) et l'algorithme JWA.
5
+ */
6
+ export interface IJwtSigningKey {
7
+ /** Clé privée Ed25519 (WebCrypto `CryptoKey`) qui signe les jetons. */
8
+ readonly key: CryptoKey;
9
+ /** Identifiant de clé = thumbprint RFC 7638 → en-tête JWT `kid` (sélection au verify). */
10
+ readonly kid: string;
11
+ /** Algorithme JWA — toujours `"EdDSA"` pour une clé Ed25519 (RFC 8037). */
12
+ readonly alg: "EdDSA";
13
+ }
14
+ /**
15
+ * Source du matériel cryptographique de signature des JWT.
16
+ *
17
+ * Découple la SIGNATURE de toute persistance concrète : la clé peut venir de
18
+ * l'environnement (prod cloud), d'un fichier (dev/VPS) ou être éphémère en
19
+ * mémoire (dev jetable) — le {@link IJwtSigningKey} et le JWKS public exposés
20
+ * sont identiques. Un keyset porte N clés (rotation) : une seule **signe** (la
21
+ * clé active), **toutes vérifient** (les jetons en vol signés par une clé encore
22
+ * présente restent valides).
23
+ *
24
+ * ⚠️ Le keystore détient des clés privées — jamais loggué, jamais sérialisé hors
25
+ * du backend choisi. Le JWKS exposé ne porte QUE des paramètres publics (RFC 8037 /
26
+ * 7517) : `kty/crv/x/kid/use/alg`, **jamais `d`**.
27
+ */
28
+ export interface IJwtKeystore {
29
+ /**
30
+ * Clé active de signature. Résout (charge ou génère) le keyset au PREMIER
31
+ * appel (lazy async, mémoïsé) — le coût de boot est nul si le JWT n'est jamais
32
+ * émis.
33
+ */
34
+ getSigningKey(): Promise<IJwtSigningKey>;
35
+ /**
36
+ * JWKS **public** (toutes les clés du keyset, paramètres publics uniquement) —
37
+ * passé à `createLocalJWKSet` pour résoudre la clé de vérification par `kid`.
38
+ */
39
+ getPublicJWKS(): Promise<JSONWebKeySet>;
40
+ }
@@ -0,0 +1,51 @@
1
+ import type { OAuth2Tokens } from "arctic";
2
+ import type { IOAuthProfile } from "@nodefony/user";
3
+ /**
4
+ * Adaptateur d'**un fournisseur OAuth/OIDC**, façade UNIFORME au-dessus d'une
5
+ * classe `arctic` — masque les divergences entre fournisseurs derrière un contrat
6
+ * stable consommé par `OAuth2Service` :
7
+ *
8
+ * - **PKCE ou non** : Google attend `createAuthorizationURL(state, codeVerifier,
9
+ * scopes)` ; GitHub `createAuthorizationURL(state, scopes)` (pas de
10
+ * `codeVerifier`). Le flag {@link usesPkce} dit au service s'il doit générer un
11
+ * `code_verifier` (RFC 7636).
12
+ * - **Extraction du profil** : OIDC décode l'ID token (Google) ; non-OIDC appelle
13
+ * l'API du fournisseur (GitHub `/user`). Le résultat est toujours normalisé en
14
+ * {@link IOAuthProfile}.
15
+ *
16
+ * @remarks `arctic` n'est référencé ici qu'en **type** (`import type`, effacé à la
17
+ * compilation) — l'instance runtime est chargée paresseusement par le service et
18
+ * injectée aux fabriques. Aucune dépendance runtime n'entre par ce contrat.
19
+ */
20
+ export interface IOAuthProvider {
21
+ /**
22
+ * `true` si le fournisseur exige PKCE (RFC 7636) — le service génère alors un
23
+ * `code_verifier` et le transmet aux deux méthodes ci-dessous.
24
+ */
25
+ readonly usesPkce: boolean;
26
+ /**
27
+ * Identifiant d'émetteur attendu pour la défense anti-mix-up (RFC 9207), ou
28
+ * `null` si le fournisseur n'émet pas le paramètre `iss` (ex. GitHub, non-OIDC).
29
+ * Quand non-`null`, le service **rejette** une réponse dont l'`iss` diffère ou
30
+ * manque.
31
+ */
32
+ readonly expectedIssuer: string | null;
33
+ /** Scopes appliqués quand la configuration n'en précise aucun. */
34
+ readonly defaultScopes: string[];
35
+ /**
36
+ * Construit l'URL d'autorisation (étape 1). `codeVerifier` est non-`null`
37
+ * lorsque {@link usesPkce} ; les fournisseurs sans PKCE l'ignorent.
38
+ */
39
+ createAuthorizationURL(state: string, codeVerifier: string | null, scopes: string[]): URL;
40
+ /**
41
+ * Échange le `code` d'autorisation contre des jetons (étape 2, canal serveur).
42
+ * `codeVerifier` doit correspondre à celui de l'étape 1 si {@link usesPkce}.
43
+ */
44
+ validateAuthorizationCode(code: string, codeVerifier: string | null): Promise<OAuth2Tokens>;
45
+ /**
46
+ * Récupère et **normalise** le profil de l'utilisateur à partir des jetons.
47
+ *
48
+ * @throws Si le fournisseur ne renvoie pas d'identifiant stable.
49
+ */
50
+ fetchProfile(tokens: OAuth2Tokens): Promise<IOAuthProfile>;
51
+ }
@@ -0,0 +1,57 @@
1
+ import type { ContextType } from "@nodefony/http";
2
+ /**
3
+ * Zone sécurisée — un pattern d'URL + la liste des authenticators à exécuter.
4
+ *
5
+ * Le firewall teste chaque zone par ordre de spécificité ; la première dont le
6
+ * pattern matche capture la requête (`context.security`). Les zones viennent de
7
+ * `defineSecurityConfig({ areas })` — patterns compilés et triés au boot, conflits
8
+ * détectés au boot (pas au premier match runtime).
9
+ */
10
+ export interface ISecuredArea {
11
+ /** Nom de la zone (`"main_api"`, `"admin"`…). */
12
+ readonly name: string;
13
+ /** Pattern d'URL compilé. */
14
+ readonly pattern: RegExp;
15
+ /** Zone protégée (Zero Trust) ; `false` = zone publique explicite. */
16
+ readonly security: boolean;
17
+ /**
18
+ * Stratégie d'identité AU-DESSUS du protocole. `false` (défaut) : registre
19
+ * serveur autorisé — session créée AU LOGIN, cookie opaque révocable (BFF).
20
+ * `true` : chaque requête porte sa preuve complète (JWT/clé API), session ignorée.
21
+ */
22
+ readonly stateless: boolean;
23
+ /**
24
+ * Sémantique de la chaîne : `"first"` = le premier authenticator qui
25
+ * reconnaît la requête authentifie ; `"all"` = tous doivent passer (MFA —
26
+ * le DERNIER porte l'identité).
27
+ */
28
+ readonly mode: "first" | "all";
29
+ /** Noms des authenticators à exécuter (sémantique selon {@link mode}). */
30
+ readonly authenticators: readonly string[];
31
+ /** Domaine/vhost de la zone (ex. `admin.exemple.com`). Omis = tous domaines. */
32
+ readonly host?: string;
33
+ /**
34
+ * URI canonique de la ressource protégée par cette zone — l'**audience**
35
+ * qu'un jeton doit porter pour y être accepté (RFC 8707 §2).
36
+ *
37
+ * Exigée par les authenticators qui vérifient un jeton émis AILLEURS : c'est
38
+ * la seule chose qui empêche un jeton parfaitement valide, délivré au même
39
+ * porteur pour un autre service, d'être rejoué ici. Elle s'ÉCRIT et ne se
40
+ * dérive pas de l'en-tête `Host` — derrière un relais, ce que le processus
41
+ * croit être son adresse n'est pas ce que le client a demandé, et l'audience
42
+ * doit être celle que le serveur d'autorisation a inscrite dans le jeton.
43
+ *
44
+ * Omise, une zone reste parfaitement utilisable par les authenticators qui
45
+ * n'en ont pas besoin (session, mot de passe, jetons maison).
46
+ */
47
+ readonly resource?: string;
48
+ /**
49
+ * Zone valable AUSSI pour le WebSocket (frames `api.request` + `subscribe`),
50
+ * pas seulement HTTP. Défaut `true` (Zero Trust : une zone protégée ferme TOUS
51
+ * ses transports) ; `false` = opt-out explicite (zone strictement HTTP). Le
52
+ * verrou WS consulte la même zone que HTTP (invariant `api.request` ≤ `GET`).
53
+ */
54
+ readonly realtime: boolean;
55
+ /** La requête tombe-t-elle dans cette zone ? (pattern + host éventuel). */
56
+ match(context: ContextType): boolean;
57
+ }
@@ -0,0 +1,41 @@
1
+ import type { IUser } from "@nodefony/user";
2
+ /**
3
+ * Jeton de sécurité — porte l'identité (authentifiée ou non) d'une requête.
4
+ *
5
+ * Produit par un {@link IAuthenticator}, propagé via ALS (`RequestContext`) pour
6
+ * tout le pipeline. Ne contient JAMAIS de credential brut après authentification :
7
+ * `getCredentials()` est vidé par l'authenticator au succès (anti-fuite mémoire).
8
+ *
9
+ * Zero Trust : un visiteur non authentifié reçoit quand même un token (porteur de
10
+ * l'`AnonymousUser`, `isAuthenticated() === false`) — `getUser()` ne renvoie jamais `null`.
11
+ */
12
+ export interface IToken {
13
+ /** Type du token (`"anonymous"`, `"userpassword"`, `"jwt"`, `"oauth2"`, `"mtls"`). */
14
+ readonly type: string;
15
+ /** Utilisateur porté — jamais `null` (AnonymousUser si non authentifié). */
16
+ getUser(): IUser;
17
+ /**
18
+ * Identifiant logique de l'utilisateur (`"anonymous"`, `"admin"`…) — partagé
19
+ * avec `IRealtimeToken` (le token WS, sous-ensemble transport-neutre, n'a PAS
20
+ * `getUser()`). Le service `authorization` ne lit QUE ce commun → la garde
21
+ * `@IsGranted` fonctionne à l'identique sur HTTP et WebSocket (« 1 garde = N
22
+ * transports »).
23
+ */
24
+ getUserIdentifier(): string;
25
+ /** `true` si l'authentification a réussi (≠ anonyme). */
26
+ isAuthenticated(): boolean;
27
+ /** Rôles **plats** de l'utilisateur (sans hiérarchie résolue). */
28
+ getRoles(): string[];
29
+ /** Credential brut avant validation (vidé au succès). */
30
+ getCredentials(): unknown;
31
+ /**
32
+ * Scopes accordés (clé API / OAuth) — axe **distinct** des rôles RBAC.
33
+ * Ex. `["repo:read", "user:email"]`. `[]` si aucun. Permet `@RequireScope(...)`
34
+ * sans confondre avec `@IsGranted('ROLE_*')` (cf GitHub PAT / OAuth scopes).
35
+ */
36
+ getScopes(): string[];
37
+ /** Lecture d'un attribut arbitraire posé par l'authenticator (claims JWT, scopes…). */
38
+ getAttribute<T = unknown>(key: string): T | undefined;
39
+ /** Pose un attribut arbitraire (claims, scopes, providerId…). */
40
+ setAttribute(key: string, value: unknown): void;
41
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * État serveur des jetons longue durée (PAT, refresh tokens), denylist des access
3
+ * tokens révoqués (`jti`) et seuil de révocation **en masse** par porteur.
4
+ *
5
+ * Pourquoi un store : un access token signé (JWT) est auto-porté et **non
6
+ * révocable** avant son `exp`. La sécurité « révocable » du mode hybride (logout
7
+ * partout, ban, fuite, rotation de refresh) **exige** un état serveur — RFC 9700
8
+ * §4.14 (rotation + détection de rejeu) + liaison scope/ressource du refresh.
9
+ * **Pluggable** par backend (mémoire/fichier/ORM/Redis) comme les stores de session.
10
+ *
11
+ * Trois structures complémentaires :
12
+ * 1. **records** ({@link IAccessTokenRecord}) — PAT + refresh persistants ;
13
+ * 2. **denylist `jti`** — révocation ciblée d'UN access avant son `exp` ;
14
+ * 3. **seuil par porteur** (`invalidBefore`) — révocation EN MASSE (tout access
15
+ * auto-porté émis avant un instant T est rejeté → « déconnexion globale » / ban).
16
+ */
17
+ import type { IPage, IPageQuery, ISortableSource } from "nodefony";
18
+ /** Raison de révocation d'un jeton — tracée pour l'audit de sécurité. */
19
+ export type TokenRevokeReason = "logout" | "rotated" | "reuse_detected" | "manual" | "compromised" | "subject_disabled" | "expired_cleanup";
20
+ /**
21
+ * Permission **fine-grained** sur une ressource (modèle GitHub fine-grained PAT).
22
+ *
23
+ * **Slot** : non exploité tant que les scopes simples suffisent ; la colonne
24
+ * existe dès maintenant pour éviter une migration le jour où on l'active.
25
+ */
26
+ export interface IResourcePermission {
27
+ /** Type de ressource (ex. `"repo"`, `"project"`). */
28
+ type: string;
29
+ /** Identifiants ciblés (omis = toutes les ressources de ce type). */
30
+ ids?: string[];
31
+ /** Permissions accordées sur ces ressources. */
32
+ perms: Array<"read" | "write">;
33
+ }
34
+ /** Contexte d'un usage de jeton (audit « last used »). */
35
+ export interface ITokenUsage {
36
+ /** Instant d'usage (epoch ms). */
37
+ at: number;
38
+ /** IP source, si disponible. */
39
+ ip?: string | null;
40
+ /** User-Agent source, si disponible. */
41
+ userAgent?: string | null;
42
+ }
43
+ /**
44
+ * Enregistrement d'un jeton persistant — un PAT (clé API style GitHub) ou un
45
+ * refresh token. Le secret n'est JAMAIS stocké en clair : seul `secretHash` vit
46
+ * ici (le secret high-entropy est affiché une seule fois à la création).
47
+ *
48
+ * Modèle **single-table** : un même schéma porte PAT et refresh ; les champs non
49
+ * pertinents pour un `kind` valent `null` (ex. `family`/`audience` pour un PAT,
50
+ * `prefix` pour un refresh opaque). Horodatages en epoch **millisecondes**
51
+ * (`Date.now()`) ; les claims JWT (secondes, RFC 7519 NumericDate) sont dérivés
52
+ * à la signature.
53
+ *
54
+ * `AccessTokenRow` (forme renvoyée par les repositories ORM) est **identique** à
55
+ * cette interface → zéro mapping entre le store et l'entité.
56
+ */
57
+ export interface IAccessTokenRecord {
58
+ /** Identifiant unique : claim `jti` (refresh) / id public (PAT). */
59
+ id: string;
60
+ /** Nature : clé API personnelle (`pat`) ou refresh token (`refresh`). */
61
+ kind: "pat" | "refresh";
62
+ /** Libellé humain (« CI deploy », « app mobile ») — affiché dans la console. */
63
+ name: string;
64
+ /** Préfixe public affichable (« nf_a1b2c3… ») pour l'UI/lookup ; `null` pour un refresh opaque. */
65
+ prefix: string | null;
66
+ /**
67
+ * Identité du porteur (id d'un utilisateur OU d'un service account).
68
+ *
69
+ * ⚠️ **Référence souple volontaire, pas une FK SQL `REFERENCES User(id)`** :
70
+ * (1) le porteur est **polymorphe** (`subjectType` user|service, futurs agents) —
71
+ * une FK ne pointe que vers une table ;
72
+ * (2) le store est **pluggable multi-backend** (tokens Redis/Mongo, users SQL) —
73
+ * une FK exigerait même base + même moteur ;
74
+ * (3) **découplage modules** — l'entité vit dans `@nodefony/security`, `User`
75
+ * dans `@nodefony/user` (user custom permis, `IUser.id` = `string|number`).
76
+ * Intégrité assurée **à l'usage** (`userProvider.load(subjectId)` → porteur
77
+ * disparu = jeton rejeté) ; suppression/ban gérés par `revokeAllForSubject`
78
+ * (révoque + audite + cross-backend, > un `ON DELETE CASCADE`).
79
+ */
80
+ subjectId: string;
81
+ /** Discriminant du porteur (humain vs machine). */
82
+ subjectType: "user" | "service";
83
+ /** Organisation/tenant (multi-tenant, façon orgs GitHub) — axe distinct du porteur ; `null` = global. */
84
+ tenantId: string | null;
85
+ /** Capacités accordées à ce jeton (⊆ droits du porteur — downscoping). Ex. `["orders:read"]`. */
86
+ scopes: string[];
87
+ /** Audiences liées (claim `aud`, RFC 8707/9700) ; `[]` = audience de l'app. Liaison conservée à la rotation. */
88
+ audience: string[];
89
+ /** Permissions fine-grained (slot GitHub) ; `null` = portée par les seuls `scopes`. */
90
+ resources: IResourcePermission[] | null;
91
+ /** Hash du secret présenté (jamais le secret) — clé de {@link ITokenStore.findByHash}. */
92
+ secretHash: string;
93
+ /** Algorithme de hachage du secret (agilité crypto, migration future). Ex. `"sha256"`. */
94
+ hashAlg: string;
95
+ /** Client OAuth émetteur (slot OAuth2/arctic) ; `null` = non-OAuth. */
96
+ clientId: string | null;
97
+ /** Confirmation sender-constrained : `jkt` (DPoP RFC 9449) / `x5t#S256` (mTLS RFC 8705) ; `null` = bearer simple. */
98
+ cnf: string | null;
99
+ /** Famille de rotation : un refresh tourné conserve la famille (reuse detection) ; `null` pour un PAT. */
100
+ family: string | null;
101
+ /** `id` du successeur après rotation (chaîne d'audit) ; `null` = feuille active. */
102
+ replacedBy: string | null;
103
+ /** Création (epoch ms). */
104
+ createdAt: number;
105
+ /** Expiration (epoch ms) ; `null` = sans expiration (PAT longue durée). */
106
+ expiresAt: number | null;
107
+ /** Dernier usage (epoch ms) ou `null`. */
108
+ lastUsedAt: number | null;
109
+ /** IP du dernier usage (audit « last used from ») ; slot. */
110
+ lastUsedIp: string | null;
111
+ /** User-Agent du dernier usage (audit) ; slot. */
112
+ lastUsedUserAgent: string | null;
113
+ /** Instant de révocation (epoch ms) ou `null` si actif. */
114
+ revokedAt: number | null;
115
+ /** Raison de révocation (audit) ou `null`. */
116
+ revokedReason: TokenRevokeReason | null;
117
+ /** Extras applicatifs libres (anti-migration), `{}` par défaut. */
118
+ metadata: Record<string, unknown>;
119
+ }
120
+ /**
121
+ * Requête de **listing paginé** de jetons (data plane admin) — le contrat de page
122
+ * standard du core ({@link IPageQuery}) enrichi des filtres propres aux jetons.
123
+ * Tous **portables** au `Criteria` (`subjectId` indexé, colonnes simples) → pas de
124
+ * SQL natif, le helper `paginate()` d'orm-core suffit côté ORM.
125
+ */
126
+ export interface ITokenListQuery extends IPageQuery {
127
+ /** Restreint à un porteur (indexé). Omis = tous porteurs. */
128
+ subjectId?: string;
129
+ /** Restreint à une nature de jeton. Omis = PAT **et** refresh. */
130
+ kind?: "pat" | "refresh";
131
+ /**
132
+ * Restreint à un **état de vie** du jeton. Omis = tous.
133
+ *
134
+ * Les trois valeurs **partitionnent** la collection (tout jeton en a
135
+ * exactement un) et sont évaluées dans cet ordre : révoqué l'emporte sur
136
+ * expiré, comme dans la console — une clé révoquée puis arrivée à échéance
137
+ * reste « révoquée », c'est l'acte d'administration qui compte.
138
+ *
139
+ * | Valeur | Signification |
140
+ * | --------- | -------------------------------------------------- |
141
+ * | `revoked` | `revokedAt` renseigné |
142
+ * | `expired` | non révoqué, `expiresAt` dépassé |
143
+ * | `active` | non révoqué, et sans échéance ou échéance à venir |
144
+ *
145
+ * Remplace l'ancien filtre `revoked` : deux vocabulaires pour le même axe
146
+ * (« révoqué ou non » et « quel état ») auraient permis `?revoked=false` +
147
+ * `?status=revoked`, une contradiction qu'un store aurait dû arbitrer — c'est
148
+ * exactement le défaut qui a rendu des compteurs faux ailleurs.
149
+ *
150
+ * **Non portable au `Criteria` générique** : `active` demande « sans échéance
151
+ * OU échéance future », un OU que le critère AND-only n'exprime pas. Chaque
152
+ * backend l'implémente donc nativement, comme le filtre `event` des webhooks.
153
+ */
154
+ status?: TokenStatus;
155
+ }
156
+ /** État de vie d'un jeton — partition exhaustive, évaluée révoqué → expiré → actif. */
157
+ export type TokenStatus = "active" | "expired" | "revoked";
158
+ /**
159
+ * Contrat du store de jetons — implémentations enregistrées via
160
+ * `registerTokenStore`, sélectionnées par config. Toutes les opérations sont
161
+ * **asynchrones** (un backend ORM/Redis fait des I/O ; la référence mémoire
162
+ * résout immédiatement).
163
+ *
164
+ * Les lectures (`findBy*`) renvoient l'enregistrement **brut** (même révoqué ou
165
+ * expiré) — la **politique** (rejet, déclenchement de la détection de rejeu) est
166
+ * du ressort de l'appelant, pas du stockage.
167
+ */
168
+ export interface ITokenStore extends ISortableSource {
169
+ /** Enregistre (ou remplace) un jeton persistant. */
170
+ put(record: IAccessTokenRecord): Promise<void>;
171
+ /** Recherche par identifiant (`jti`/id public). `null` si absent. */
172
+ findById(id: string): Promise<IAccessTokenRecord | null>;
173
+ /** Recherche par hash de secret présenté. `null` si absent. */
174
+ findByHash(secretHash: string): Promise<IAccessTokenRecord | null>;
175
+ /** Tous les jetons d'un porteur (console « mes jetons », révocation ciblée). */
176
+ findBySubject(subjectId: string): Promise<IAccessTokenRecord[]>;
177
+ /**
178
+ * **Tous** les jetons du store, tous porteurs confondus — vue
179
+ * d'ADMINISTRATION cross-porteur (gouvernance / réponse à incident).
180
+ *
181
+ * ⚠️ Énumération COMPLÈTE : opération de cold-path réservée à la console admin
182
+ * (RBAC `ROLE_NODEFONY_ADMIN`), jamais sur le hot-path d'authentification. Un
183
+ * backend distribué (Redis) l'implémente par SCAN ; à grande échelle, paginer
184
+ * en amont. Renvoie les records BRUTS (la projection « sans secret » est du
185
+ * ressort de l'appelant — `ApiKeyService.listAllPat`).
186
+ */
187
+ listAll(): Promise<IAccessTokenRecord[]>;
188
+ /**
189
+ * Liste **paginée** de jetons pour le data plane admin — ne matérialise **jamais**
190
+ * plus d'une page (contrairement à {@link ITokenStore.listAll}, réservé au dump
191
+ * d'incident). Filtres {@link ITokenListQuery} appliqués au store.
192
+ *
193
+ * Capacité par backend : **offset + total** (SQL/Mongo/mémoire, tri `createdAt`
194
+ * DESC déterministe + tiebreaker `id`) ; **curseur** (`nextCursor`, Redis SCAN —
195
+ * sans total ni ordre global, capacité réduite annoncée). Renvoie les records
196
+ * BRUTS (la projection « sans secret » = ressort de l'appelant).
197
+ */
198
+ listPage(query: ITokenListQuery): Promise<IPage<IAccessTokenRecord>>;
199
+ /**
200
+ * Compte les jetons correspondant aux filtres — base du `total` de la page
201
+ * (SQL/Mongo `COUNT`, mémoire `length`). Redis : `-1` (comptage O(N) refusé).
202
+ */
203
+ countTokens(query: ITokenListQuery): Promise<number>;
204
+ /** Met à jour `lastUsedAt`/IP/UA (no-op si l'id est inconnu). */
205
+ markUsed(id: string, usage: ITokenUsage): Promise<void>;
206
+ /** Révoque un jeton (pose `revokedAt`+`revokedReason`) — idempotent. */
207
+ revoke(id: string, reason: TokenRevokeReason): Promise<void>;
208
+ /** Révoque TOUTE la famille de rotation (détection de rejeu, RFC 9700). */
209
+ revokeFamily(family: string, reason: TokenRevokeReason): Promise<void>;
210
+ /**
211
+ * Inscrit un `jti` d'access sur la denylist jusqu'à `expiresAt` (epoch ms) —
212
+ * révocation immédiate avant l'`exp` du JWT. Au-delà, l'entrée est inutile.
213
+ */
214
+ denyJti(jti: string, expiresAt: number): Promise<void>;
215
+ /** `true` si le `jti` est denylisté et non encore expiré. */
216
+ isJtiDenied(jti: string): Promise<boolean>;
217
+ /**
218
+ * Pose le seuil `invalidBefore` (epoch ms) d'un porteur : tout access auto-porté
219
+ * dont `iat < invalidBefore` est rejeté. Couvre les JWT non stockés sans
220
+ * denylister chaque `jti` (refresh/PAT se révoquent via `revoke`/`revokeFamily`).
221
+ */
222
+ revokeAllForSubject(subjectId: string, invalidBefore: number): Promise<void>;
223
+ /** Seuil `invalidBefore` d'un porteur (epoch ms) ou `null` si aucun. */
224
+ getInvalidBefore(subjectId: string): Promise<number | null>;
225
+ /**
226
+ * Purge les entrées devenues inutiles : denylist `jti` expirée, records arrivés
227
+ * à `exp`, PAT révoqués sans expiration au-delà de la fenêtre de rétention. Un
228
+ * refresh révoqué par rotation est conservé jusqu'à son `exp` (= fenêtre de
229
+ * détection de rejeu), puis purgé.
230
+ *
231
+ * **Déclenchement** : à planifier périodiquement par l'orchestrateur du store
232
+ * (timer `unref` posé au boot, nettoyé à l'arrêt). Les backends à TTL natif
233
+ * (Redis `EXPIRE`, Mongo TTL index) gèrent l'expiration eux-mêmes → leur `gc()`
234
+ * se limite aux cas non couverts par le TTL. **Sans orchestrateur le store
235
+ * s'accumule** — ne jamais laisser ce seam vide.
236
+ *
237
+ * @returns nombre d'entrées purgées.
238
+ */
239
+ gc(now?: number): Promise<number>;
240
+ }