@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,145 @@
1
+ import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
+ import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
3
+ import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, readStoreLocation, resolveAutoStore } from "nodefony";
4
+ import { randomBytes } from "node:crypto";
5
+ //#region nodefony/service/auditService.ts
6
+ const serviceName = "auditService";
7
+ const GC_INTERVAL_MS = 36e5;
8
+ /**
9
+ * Journal d'audit de sécurité (P6.14) — collecte les **événements** de sécurité
10
+ * (login, refus d'accès, jeton émis/révoqué, défense CSRF/CORS, verrou WS) émis
11
+ * EXPLICITEMENT par le firewall, les authenticators et les controllers. Distinct
12
+ * du log de trafic (`JsonAuditLogger`, P3.1, 1 PDU/requête) : ici on trace les
13
+ * **transitions d'état** de sécurité, jamais le hot-path par requête.
14
+ *
15
+ * Propriétaire du {@link IAuditStore} (référence mémoire append-only) : le pose au
16
+ * container (`auditStore`, consommé par le data plane P6.15) et arme le `gc` de
17
+ * rétention (timer `unref`). Implémente {@link IAuditSink} — `record` est
18
+ * **fire-and-forget** (jamais bloquant) et **no-op à coût nul** si désactivé.
19
+ *
20
+ * Slot : store **pluggable** (ORM/Loki, multi-pod) = lot futur, comme
21
+ * `tokenStoreRegistry`. Le socle n'embarque que la référence mémoire.
22
+ */
23
+ var AuditService = class extends Service {
24
+ module;
25
+ #store = null;
26
+ #enabled = false;
27
+ /** Abonnés au flux live — `null` tant qu'aucun (lazy, règle hooks). */
28
+ #listeners = null;
29
+ /** Préfixe d'id unique au process (1 seul `randomBytes` au boot) + compteur. */
30
+ #idPrefix = "";
31
+ #seq = 0;
32
+ #gc = null;
33
+ constructor(module) {
34
+ super(serviceName, module.container, module.notificationsCenter, module.options);
35
+ this.module = module;
36
+ this.kernel?.once("onBoot", () => this.#build());
37
+ this.kernel?.once("onTerminate", () => this.#shutdown());
38
+ }
39
+ #build() {
40
+ let config;
41
+ try {
42
+ config = defineSecurityConfig(this.options);
43
+ } catch {
44
+ return;
45
+ }
46
+ if (!config.audit.enabled) {
47
+ this.log("audit service idle — désactivé par la config", "DEBUG");
48
+ return;
49
+ }
50
+ let storeName = config.audit.store;
51
+ let reason = `store explicitement configuré ("${storeName}")`;
52
+ if (storeName === AUTO_STORE) {
53
+ const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores());
54
+ storeName = auto.store;
55
+ reason = auto.reason;
56
+ this.log(`audit.store "auto" → "${storeName}" (${auto.reason})`, "INFO");
57
+ }
58
+ const factory = getAuditStoreFactory(storeName);
59
+ if (!factory) {
60
+ const msg = `audit store "${storeName}" inconnu (enregistrés : ${listAuditStores().join(", ") || "aucun"})`;
61
+ if (this.kernel?.environment === "production") throw new Error(`${msg} — journal de sécurité non collecté : boot avorté.`);
62
+ this.log(`${msg} — audit désactivé (journal de sécurité non collecté)`, "CRITIC");
63
+ return;
64
+ }
65
+ if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. Déclarer une infra durable (NF_DATABASE_URL).", "WARNING");
66
+ this.#enabled = true;
67
+ this.#idPrefix = randomBytes(4).toString("hex");
68
+ this.#store = factory({
69
+ container: this.container,
70
+ config
71
+ });
72
+ this.container?.set("auditStore", this.#store);
73
+ this.kernel?.registerStoreResolution({
74
+ brick: "audit",
75
+ nature: "durable",
76
+ configured: config.audit.store,
77
+ resolved: storeName,
78
+ available: listAuditStores(),
79
+ reason,
80
+ configPath: "security.audit.store",
81
+ location: readStoreLocation(this.#store)
82
+ });
83
+ this.#gc = new GcScheduler({
84
+ intervalS: GC_INTERVAL_MS / 1e3,
85
+ jitter: true,
86
+ run: async () => {
87
+ const purged = await this.#store?.gc();
88
+ if (purged && purged > 0) this.log(`audit gc — ${purged} événement(s) purgé(s)`, "DEBUG");
89
+ },
90
+ onError: (e) => this.log(e, "WARNING")
91
+ });
92
+ this.#gc.start();
93
+ this.log(`audit service ready — store "${config.audit.store}", rétention ${config.audit.retentionDays}j`, "DEBUG");
94
+ }
95
+ #shutdown() {
96
+ this.#gc?.stop();
97
+ this.#gc = null;
98
+ this.#listeners = null;
99
+ this.#store = null;
100
+ this.#enabled = false;
101
+ }
102
+ record(draft) {
103
+ if (!this.#enabled || this.#store === null) return;
104
+ const event = {
105
+ ...draft,
106
+ id: `${this.#idPrefix}-${(this.#seq++).toString(36)}`,
107
+ ts: Date.now()
108
+ };
109
+ this.#store.append(event).catch((error) => this.log(error, "ERROR"));
110
+ if (this.#listeners !== null) for (let i = 0; i < this.#listeners.length; i++) try {
111
+ this.#listeners[i](event);
112
+ } catch (error) {
113
+ this.log(error, "ERROR");
114
+ }
115
+ }
116
+ subscribe(listener) {
117
+ if (this.#listeners === null) this.#listeners = [];
118
+ this.#listeners.push(listener);
119
+ let active = true;
120
+ return () => {
121
+ if (!active || this.#listeners === null) return;
122
+ active = false;
123
+ const idx = this.#listeners.indexOf(listener);
124
+ if (idx >= 0) this.#listeners.splice(idx, 1);
125
+ if (this.#listeners.length === 0) this.#listeners = null;
126
+ };
127
+ }
128
+ /** Lit une page du journal (délègue au store) ; vide si l'audit est inactif. */
129
+ listPage(query) {
130
+ if (this.#store === null) return Promise.resolve({
131
+ items: [],
132
+ limit: query.limit,
133
+ hasNext: false,
134
+ nextCursor: null,
135
+ total: 0
136
+ });
137
+ return this.#store.listPage(query);
138
+ }
139
+ /** `true` si l'audit est actif (config `audit.enabled`). */
140
+ isEnabled() {
141
+ return this.#enabled;
142
+ }
143
+ };
144
+ //#endregion
145
+ export { AuditService as default };
@@ -0,0 +1,332 @@
1
+ import { AuthenticationError } from "../errors/AuthenticationError.js";
2
+ import { ThrottledError } from "../errors/ThrottledError.js";
3
+ import { resolveSessionIdentity } from "../src/sessionIdentity.js";
4
+ import { recordAudit } from "../src/audit/recordAudit.js";
5
+ import { readAuditContext } from "../src/audit/readAuditContext.js";
6
+ import { RequestContext, Service } from "nodefony";
7
+ //#region nodefony/service/authFlow.ts
8
+ const serviceName = "authFlow";
9
+ const INVALID_CREDENTIALS = "Invalid credentials";
10
+ const PENDING_MFA_KEY = "mfa:pending";
11
+ /**
12
+ * Flux de session BFF — login/logout/me côté serveur (P6 J3).
13
+ *
14
+ * C'est le GUICHET : le credential est présenté UNE fois (`login`), vérifié par
15
+ * le {@link IPasswordVerifier} (hash, leurre anti-timing, re-hash migration),
16
+ * puis remplacé par un cookie de session opaque `HttpOnly` — le mot de passe ne
17
+ * recircule jamais, le navigateur ne stocke aucun token lisible par JS.
18
+ *
19
+ * Anti session-fixation (OWASP) : l'ID de session est TOUJOURS régénéré au
20
+ * login — un ID pré-posé par un attaquant (cookie forcé avant le guichet) ne
21
+ * survit pas à l'authentification ; l'ancienne entrée storage est détruite.
22
+ *
23
+ * Throttling NIST SP 800-63B : le MÊME `LoginThrottler` que la porte Basic
24
+ * (instance partagée via le container, posée par le firewall au boot) — un
25
+ * attaquant ne contourne pas le backoff en changeant de porte.
26
+ *
27
+ * Les handlers HTTP (`SessionAuthController`, `@nodefony/framework`) sont des
28
+ * adaptateurs minces au-dessus de ce service — la logique reste testable sans
29
+ * transport.
30
+ */
31
+ var AuthFlow = class extends Service {
32
+ module;
33
+ #users = null;
34
+ #sessions = null;
35
+ #throttler = null;
36
+ #throttlerResolved = false;
37
+ #totp = null;
38
+ #totpResolved = false;
39
+ constructor(module) {
40
+ super(serviceName, module.container, module.notificationsCenter, module.options);
41
+ this.module = module;
42
+ }
43
+ /**
44
+ * Authentifie le couple identifiant/mot de passe et OUVRE la session BFF.
45
+ *
46
+ * Ordre NIST : throttle AVANT le verifier (un identifiant bloqué ne coûte
47
+ * aucun hash argon2 — le backoff protège aussi le serveur du DoS), échec
48
+ * compté, succès remis à zéro.
49
+ *
50
+ * @param context - contexte HTTP courant (porte la session/le cookie).
51
+ * @param identifier - identifiant saisi (body JSON, non typé à la frontière).
52
+ * @param password - mot de passe saisi.
53
+ * @returns `authenticated` (identité établie) ou `mfa_required` (2ᵉ facteur requis).
54
+ * @throws ThrottledError (429 + `Retry-After`) — backoff actif.
55
+ * @throws AuthenticationError (401, message uniforme) — credential absent ou
56
+ * invalide.
57
+ */
58
+ async login(context, identifier, password) {
59
+ const info = readAuditContext(context);
60
+ const who = typeof identifier === "string" ? identifier : null;
61
+ if (typeof identifier !== "string" || identifier.length === 0 || typeof password !== "string" || password.length === 0) {
62
+ recordAudit(this.container, {
63
+ category: "auth",
64
+ action: "login.failure",
65
+ outcome: "failure",
66
+ actor: who,
67
+ reason: "invalid_credentials",
68
+ ...info
69
+ });
70
+ throw new AuthenticationError(INVALID_CREDENTIALS);
71
+ }
72
+ const throttler = this.#resolveThrottler();
73
+ if (throttler !== null) {
74
+ const retryAfterS = throttler.check(identifier);
75
+ if (retryAfterS > 0) {
76
+ recordAudit(this.container, {
77
+ category: "auth",
78
+ action: "login.throttled",
79
+ outcome: "failure",
80
+ actor: identifier,
81
+ reason: "throttled",
82
+ ...info
83
+ });
84
+ throw new ThrottledError(retryAfterS);
85
+ }
86
+ }
87
+ const user = await this.#resolveUsers().authenticate(identifier, password);
88
+ if (user === null) {
89
+ throttler?.recordFailure(identifier);
90
+ recordAudit(this.container, {
91
+ category: "auth",
92
+ action: "login.failure",
93
+ outcome: "failure",
94
+ actor: identifier,
95
+ reason: "invalid_credentials",
96
+ ...info
97
+ });
98
+ throw new AuthenticationError(INVALID_CREDENTIALS);
99
+ }
100
+ throttler?.recordSuccess(identifier);
101
+ const totp = this.#resolveTotp();
102
+ if (totp && await totp.isEnabledFor(user.identifier)) {
103
+ const session = await this.ensureSession(context);
104
+ session?.set(PENDING_MFA_KEY, user.identifier);
105
+ await session?.save();
106
+ recordAudit(this.container, {
107
+ category: "auth",
108
+ action: "login.mfa_required",
109
+ outcome: "success",
110
+ actor: user.identifier,
111
+ reason: "totp",
112
+ ...info
113
+ });
114
+ return {
115
+ status: "mfa_required",
116
+ methods: ["totp"]
117
+ };
118
+ }
119
+ await this.#openSession(context, user.identifier);
120
+ context.user = user.identifier;
121
+ RequestContext.set("user", user);
122
+ recordAudit(this.container, {
123
+ category: "auth",
124
+ action: "login.success",
125
+ outcome: "success",
126
+ actor: user.identifier,
127
+ ...info
128
+ });
129
+ return {
130
+ status: "authenticated",
131
+ user: toSafeUser(user)
132
+ };
133
+ }
134
+ /**
135
+ * Ouvre la session BFF pour un utilisateur **déjà authentifié par un autre
136
+ * facteur** (passkey/WebAuthn, OAuth, magic link…) — aucun mot de passe à
137
+ * vérifier ici, la preuve a été apportée en amont par l'appelant.
138
+ *
139
+ * Même anti-fixation que {@link login} (ID régénéré, ancienne entrée
140
+ * détruite). L'identité est re-résolue + revalidée (compte actif/non
141
+ * verrouillé) via la source unique `resolveSessionIdentity` — un compte banni
142
+ * entre la preuve et l'ouverture de session est rejeté.
143
+ *
144
+ * @param identifier - identifiant de l'utilisateur prouvé (ex. `sub` du credential).
145
+ * @throws AuthenticationError (401, message uniforme) — identifiant absent, ou
146
+ * compte disparu/inactif/verrouillé.
147
+ */
148
+ async establishSessionFor(context, identifier, reason = "federated") {
149
+ if (typeof identifier !== "string" || identifier.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
150
+ const user = await resolveSessionIdentity(this.#resolveUsers(), identifier);
151
+ await this.#openSession(context, user.identifier);
152
+ context.user = user.identifier;
153
+ RequestContext.set("user", user);
154
+ recordAudit(this.container, {
155
+ category: "auth",
156
+ action: "login.success",
157
+ outcome: "success",
158
+ actor: user.identifier,
159
+ reason,
160
+ ...readAuditContext(context)
161
+ });
162
+ return toSafeUser(user);
163
+ }
164
+ /**
165
+ * Valide le **second facteur** (code TOTP ou code de récupération) après un
166
+ * `login` ayant renvoyé `mfa_required`, puis OUVRE la session BFF. Le défi
167
+ * PENDING déposé en session par `login` est lu, vérifié, puis invalidé (usage
168
+ * unique) ; l'identité n'est établie qu'ICI. **Throttlé** sur l'identité en
169
+ * attente (anti brute-force du code à 6 chiffres).
170
+ *
171
+ * @param context - contexte HTTP (porte la session PENDING).
172
+ * @param code - code présenté (TOTP ou code de récupération).
173
+ * @returns la projection publique de l'utilisateur authentifié.
174
+ * @throws ThrottledError (429) — trop de tentatives.
175
+ * @throws AuthenticationError (401, uniforme) — aucun défi en cours, code absent
176
+ * ou invalide (la session N'est PAS ouverte ; le défi reste pour un retry).
177
+ */
178
+ async completeMfaLogin(context, code) {
179
+ const info = readAuditContext(context);
180
+ const pending = context.session?.get(PENDING_MFA_KEY);
181
+ if (typeof pending !== "string" || pending.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
182
+ const throttler = this.#resolveThrottler();
183
+ if (throttler !== null) {
184
+ const retryAfterS = throttler.check(pending);
185
+ if (retryAfterS > 0) {
186
+ recordAudit(this.container, {
187
+ category: "auth",
188
+ action: "login.throttled",
189
+ outcome: "failure",
190
+ actor: pending,
191
+ reason: "throttled",
192
+ ...info
193
+ });
194
+ throw new ThrottledError(retryAfterS);
195
+ }
196
+ }
197
+ const totp = this.#resolveTotp();
198
+ const result = typeof code === "string" && code.length > 0 && totp ? await totp.verifyLogin(pending, code) : {
199
+ ok: false,
200
+ method: void 0
201
+ };
202
+ if (!result.ok) {
203
+ throttler?.recordFailure(pending);
204
+ recordAudit(this.container, {
205
+ category: "auth",
206
+ action: "login.failure",
207
+ outcome: "failure",
208
+ actor: pending,
209
+ reason: "mfa_invalid",
210
+ ...info
211
+ });
212
+ throw new AuthenticationError(INVALID_CREDENTIALS);
213
+ }
214
+ throttler?.recordSuccess(pending);
215
+ context.session?.set(PENDING_MFA_KEY, null);
216
+ return this.establishSessionFor(context, pending, result.method ?? "totp");
217
+ }
218
+ /**
219
+ * Garantit une session pour la requête courante — la démarre (+ cookie) si
220
+ * elle n'existe pas encore. Sert aux cérémonies **pré-authentification**
221
+ * (login WebAuthn) : elles doivent porter un challenge côté serveur AVANT que
222
+ * l'utilisateur soit connecté. La session anonyme ainsi créée devient la
223
+ * session authentifiée au login — {@link establishSessionFor} régénère l'ID
224
+ * (anti-fixation préservée), donc un challenge déposé ici n'ouvre aucune brèche.
225
+ *
226
+ * @returns la session (existante ou neuve), ou `null` si le service de session
227
+ * est indisponible.
228
+ */
229
+ async ensureSession(context) {
230
+ if (context.session) return context.session;
231
+ return this.#resolveSessions().start(context);
232
+ }
233
+ /**
234
+ * Détruit la session courante (storage + cookie). Idempotent : sans session
235
+ * active, ne fait rien.
236
+ *
237
+ * @returns `true` si une session a réellement été détruite.
238
+ */
239
+ async logout(context) {
240
+ const session = context.session;
241
+ if (!session || session.status !== "active") return false;
242
+ const actor = typeof session.user === "string" ? session.user : null;
243
+ await session.destroy(true);
244
+ context.session = null;
245
+ context.user = null;
246
+ recordAudit(this.container, {
247
+ category: "session",
248
+ action: "logout",
249
+ outcome: "success",
250
+ actor,
251
+ ...readAuditContext(context)
252
+ });
253
+ return true;
254
+ }
255
+ /**
256
+ * Identité portée par la session courante, re-résolue auprès du provider
257
+ * (mêmes contrôles que le `SessionAuthenticator` — source unique).
258
+ *
259
+ * @returns la projection publique, ou `null` (pas de session, session
260
+ * orpheline, compte verrouillé/désactivé) — le handler répond 401.
261
+ */
262
+ async me(context) {
263
+ const identifier = context.session?.user;
264
+ if (typeof identifier !== "string" || identifier.length === 0) return null;
265
+ try {
266
+ return toSafeUser(await resolveSessionIdentity(this.#resolveUsers(), identifier));
267
+ } catch (error) {
268
+ if (error instanceof AuthenticationError) return null;
269
+ throw error;
270
+ }
271
+ }
272
+ async #openSession(context, identifier) {
273
+ const session = context.session ?? await this.#resolveSessions().start(context);
274
+ if (session === null) {
275
+ this.log("login: session service returned no session", "ERROR");
276
+ throw new AuthenticationError(INVALID_CREDENTIALS);
277
+ }
278
+ const oldId = session.id;
279
+ session.regenerateId();
280
+ try {
281
+ await session.storage.destroy(oldId);
282
+ } catch {}
283
+ const provenance = context;
284
+ try {
285
+ const ip = provenance.getRemoteAddress?.();
286
+ if (ip) session.setMetaBag("ip", ip);
287
+ const ua = provenance.getUserAgent?.();
288
+ if (ua) session.setMetaBag("ua", ua);
289
+ } catch {}
290
+ await session.save(identifier);
291
+ }
292
+ #resolveSessions() {
293
+ if (this.#sessions === null) {
294
+ const sessions = this.get("sessions");
295
+ if (!sessions) throw new Error("AuthFlow: aucun service \"sessions\" dans le container — le module @nodefony/http doit être chargé avant @nodefony/security.");
296
+ this.#sessions = sessions;
297
+ }
298
+ return this.#sessions;
299
+ }
300
+ #resolveUsers() {
301
+ if (this.#users === null) {
302
+ const users = this.get("users");
303
+ if (!users) throw new Error("AuthFlow: aucun service \"users\" (IPasswordVerifier & IUserProvider) dans le container — enregistrer un UserService au boot de l'application.");
304
+ this.#users = users;
305
+ }
306
+ return this.#users;
307
+ }
308
+ #resolveThrottler() {
309
+ if (!this.#throttlerResolved) {
310
+ this.#throttler = this.get("loginThrottler") ?? null;
311
+ this.#throttlerResolved = true;
312
+ }
313
+ return this.#throttler;
314
+ }
315
+ #resolveTotp() {
316
+ if (!this.#totpResolved) {
317
+ const svc = this.get("totp") ?? null;
318
+ this.#totp = svc && svc.isEnabled() ? svc : null;
319
+ this.#totpResolved = true;
320
+ }
321
+ return this.#totp;
322
+ }
323
+ };
324
+ function toSafeUser(user) {
325
+ return {
326
+ id: user.id,
327
+ username: user.identifier,
328
+ roles: [...user.roles]
329
+ };
330
+ }
331
+ //#endregion
332
+ export { AuthFlow, AuthFlow as default };
@@ -0,0 +1,95 @@
1
+ import { recordAudit } from "../src/audit/recordAudit.js";
2
+ import "../contracts/IAccessVoter.js";
3
+ import { listVoterFactories } from "../src/voter/voterRegistry.js";
4
+ import { Service, logColor } from "nodefony";
5
+ //#region nodefony/service/authorization.ts
6
+ const serviceName = "authorization";
7
+ /**
8
+ * Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
9
+ * jury de {@link IAccessVoter}.
10
+ *
11
+ * Stratégie **affirmative + DENY veto** : un seul `DENY` bloque (veto) ; sinon un
12
+ * `GRANT` suffit ; **silence total** (tous `ABSTAIN`, ou aucun voter compétent)
13
+ * → `DENY` (**Zero Trust** : fermé par défaut). Tout refus est audité (WARNING) ;
14
+ * les accès accordés restent silencieux (pas de spam — l'audit d'octroi explicite
15
+ * viendra avec `@AuditLog`, P6.14).
16
+ *
17
+ * Les voters sont découverts au boot via le `voterRegistry` (built-in `role` +
18
+ * ceux des apps/plugins) — aucun nom en dur ici. Consommé par les décorateurs
19
+ * (`@IsGranted`, J7) et le verrou de frame WS (« 1 garde = N transports »).
20
+ *
21
+ * Perf : aucune allocation par appel (`decide` itère les voters et teste
22
+ * `supports()` en place) ; les voters sont instanciés UNE fois au boot.
23
+ */
24
+ var Authorization = class extends Service {
25
+ module;
26
+ #voters = null;
27
+ constructor(module) {
28
+ super(serviceName, module.container, module.notificationsCenter, module.options);
29
+ this.module = module;
30
+ this.kernel?.once("onBoot", () => this.#build());
31
+ }
32
+ #build() {
33
+ const list = [];
34
+ const ctx = { container: this.container };
35
+ for (const [, factory] of listVoterFactories()) list.push(factory(ctx));
36
+ this.#voters = list;
37
+ this.log(`Authorization ready — ${list.length} voter(s)`, "DEBUG");
38
+ return list;
39
+ }
40
+ /**
41
+ * Le token a-t-il le droit `attribute` sur `subject` ? Affirmative + DENY veto,
42
+ * défaut `DENY` (Zero Trust). Voir {@link IAuthorizationService.decide}.
43
+ */
44
+ async decide(token, attribute, subject) {
45
+ const voters = this.#voters ?? this.#build();
46
+ let granted = false;
47
+ let considered = 0;
48
+ for (let i = 0; i < voters.length; i++) {
49
+ const voter = voters[i];
50
+ if (!voter.supports(attribute, subject)) continue;
51
+ considered++;
52
+ let vote;
53
+ try {
54
+ vote = await voter.vote(token, attribute, subject);
55
+ } catch (error) {
56
+ this.log(error, "ERROR");
57
+ this.#auditDeny(token, attribute, subject, "error");
58
+ return false;
59
+ }
60
+ if (vote === "DENY") {
61
+ this.#auditDeny(token, attribute, subject, "veto");
62
+ return false;
63
+ }
64
+ if (vote === "GRANT") granted = true;
65
+ }
66
+ if (!granted) this.#auditDeny(token, attribute, subject, considered === 0 ? "no-voter" : "abstain");
67
+ return granted;
68
+ }
69
+ #auditDeny(token, attribute, subject, reason) {
70
+ const who = token.getUserIdentifier();
71
+ const on = subject === void 0 ? "" : ` on ${describeSubject(subject)}`;
72
+ this.log(`access denied: "${who}" → "${attribute}"${on} (${reason})`, "WARNING");
73
+ recordAudit(this.container, {
74
+ category: "authz",
75
+ action: "access.denied",
76
+ outcome: "denied",
77
+ actor: who,
78
+ resource: attribute,
79
+ reason,
80
+ metadata: subject === void 0 ? void 0 : { subject: describeSubject(subject) }
81
+ });
82
+ }
83
+ log(pci, severity, msgid, msg) {
84
+ if (!msgid) msgid = logColor.cyan("AUTHORIZATION");
85
+ return super.log(pci, severity, msgid, msg);
86
+ }
87
+ };
88
+ /** Descripteur léger d'un sujet pour l'audit (jamais de JSON.stringify aveugle). */
89
+ function describeSubject(subject) {
90
+ if (typeof subject === "string") return subject;
91
+ if (subject === null) return "null";
92
+ return subject?.constructor?.name ?? typeof subject;
93
+ }
94
+ //#endregion
95
+ export { Authorization, Authorization as default };
@@ -0,0 +1,81 @@
1
+ //#region nodefony/service/cors.ts
2
+ /**
3
+ * Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
4
+ * (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
5
+ * en-têtes `Access-Control-*` à poser pour une origine donnée. Instanciée une
6
+ * fois au boot par le firewall, testable sans serveur.
7
+ *
8
+ * Invariants de sécurité (OWASP) :
9
+ * - **Jamais `*` + credentials** : interdit au boot (refine Zod). Ici, `*` n'est
10
+ * émis QUE si `credentials=false` ; avec credentials, l'origine est reflétée.
11
+ * - **Reflet d'origine ⇒ `Vary: Origin`** : signalé via {@link reflectsOrigin}
12
+ * pour que l'appelant pose l'en-tête `Vary` (correction de cache).
13
+ * - **Origine non whitelistée ⇒ aucun en-tête** : la réponse n'est pas partageable
14
+ * (le navigateur bloque), zéro information divulguée.
15
+ *
16
+ * @see Fetch Standard (CORS protocol) · OWASP CORS.
17
+ */
18
+ var Cors = class {
19
+ #origins;
20
+ #wildcard;
21
+ #credentials;
22
+ #methods;
23
+ #allowedHeaders;
24
+ #exposedHeaders;
25
+ #maxAge;
26
+ constructor(options) {
27
+ this.#origins = new Set(options.origins);
28
+ this.#wildcard = options.origins.includes("*");
29
+ this.#credentials = options.credentials;
30
+ this.#methods = options.methods.join(", ");
31
+ this.#allowedHeaders = options.allowedHeaders.join(", ");
32
+ this.#exposedHeaders = options.exposedHeaders.join(", ");
33
+ this.#maxAge = String(options.maxAgeS);
34
+ }
35
+ /**
36
+ * Valeur de `Access-Control-Allow-Origin` pour cette origine, ou `null` si
37
+ * l'origine n'est pas autorisée. `*` seulement en l'absence de credentials
38
+ * (sinon l'origine est reflétée — `*` + credentials est refusé par le navigateur).
39
+ */
40
+ #allowOrigin(origin) {
41
+ if (this.#wildcard) return this.#credentials ? origin : "*";
42
+ return this.#origins.has(origin) ? origin : null;
43
+ }
44
+ /** `true` si la valeur Allow-Origin reflète l'origine (⇒ l'appelant doit poser `Vary: Origin`). */
45
+ reflectsOrigin(allowOrigin) {
46
+ return allowOrigin !== "*";
47
+ }
48
+ /**
49
+ * En-têtes d'une réponse au **preflight** `OPTIONS` (méthodes/headers autorisés
50
+ * + cache + credentials), ou `null` si l'origine n'est pas autorisée (réponse
51
+ * 204 nue → le navigateur bloque).
52
+ */
53
+ preflightHeaders(origin) {
54
+ const allow = this.#allowOrigin(origin);
55
+ if (allow === null) return null;
56
+ const h = {
57
+ "Access-Control-Allow-Origin": allow,
58
+ "Access-Control-Allow-Methods": this.#methods,
59
+ "Access-Control-Allow-Headers": this.#allowedHeaders,
60
+ "Access-Control-Max-Age": this.#maxAge
61
+ };
62
+ if (this.reflectsOrigin(allow)) h["Vary"] = "Origin";
63
+ if (this.#credentials) h["Access-Control-Allow-Credentials"] = "true";
64
+ return h;
65
+ }
66
+ /**
67
+ * En-têtes d'une réponse à une **requête réelle** cross-origin (Allow-Origin +
68
+ * credentials + headers exposés au JS), ou `null` si l'origine n'est pas autorisée.
69
+ */
70
+ actualHeaders(origin) {
71
+ const allow = this.#allowOrigin(origin);
72
+ if (allow === null) return null;
73
+ const h = { "Access-Control-Allow-Origin": allow };
74
+ if (this.reflectsOrigin(allow)) h["Vary"] = "Origin";
75
+ if (this.#credentials) h["Access-Control-Allow-Credentials"] = "true";
76
+ if (this.#exposedHeaders) h["Access-Control-Expose-Headers"] = this.#exposedHeaders;
77
+ return h;
78
+ }
79
+ };
80
+ //#endregion
81
+ export { Cors, Cors as default };