@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4

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 (249) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +48 -16
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
  4. package/dist/nodefony/command/security-oauth-doctor.js +68 -0
  5. package/dist/nodefony/command/security-secrets.js +52 -29
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +10 -7
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +9 -14
  10. package/dist/nodefony/config/config.js +99 -8
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/contracts/index.js +1 -2
  14. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  15. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  16. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  17. package/dist/nodefony/errors/CsrfError.js +1 -2
  18. package/dist/nodefony/errors/InsecureTransportError.js +23 -0
  19. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  20. package/dist/nodefony/errors/SsrfError.js +1 -2
  21. package/dist/nodefony/errors/ThrottledError.js +1 -2
  22. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  23. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  24. package/dist/nodefony/errors/index.js +2 -1
  25. package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
  26. package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
  27. package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
  28. package/dist/nodefony/keycloak/scaffold.js +231 -0
  29. package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
  30. package/dist/nodefony/scaffold/contribute.js +19 -0
  31. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  32. package/dist/nodefony/service/apiKeys.js +45 -5
  33. package/dist/nodefony/service/auditService.js +4 -5
  34. package/dist/nodefony/service/authFlow.js +118 -16
  35. package/dist/nodefony/service/authorization.js +1 -2
  36. package/dist/nodefony/service/cors.js +1 -2
  37. package/dist/nodefony/service/csrf.js +1 -2
  38. package/dist/nodefony/service/firewall.js +116 -25
  39. package/dist/nodefony/service/keycloak.js +208 -0
  40. package/dist/nodefony/service/oauth2.js +403 -28
  41. package/dist/nodefony/service/securityHeaders.js +1 -2
  42. package/dist/nodefony/service/tokenService.js +94 -27
  43. package/dist/nodefony/service/totp.js +55 -11
  44. package/dist/nodefony/service/webAuthn.js +24 -7
  45. package/dist/nodefony/service/webhooks.js +4 -5
  46. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  47. package/dist/nodefony/src/SecuredArea.js +15 -5
  48. package/dist/nodefony/src/accountBinding.js +96 -0
  49. package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
  50. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  51. package/dist/nodefony/src/admin/adminAudit.js +1 -3
  52. package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
  53. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  54. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  55. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  56. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  57. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  58. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  59. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  60. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  61. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  62. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
  63. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  64. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
  65. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
  66. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  67. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
  68. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
  69. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  70. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  71. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  72. package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
  73. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  74. package/dist/nodefony/src/csp.js +1 -2
  75. package/dist/nodefony/src/csrfToken.js +1 -2
  76. package/dist/nodefony/src/entryPoint.js +38 -0
  77. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  78. package/dist/nodefony/src/net/transportFailure.js +57 -0
  79. package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
  80. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  81. package/dist/nodefony/src/oauth/metadata.js +28 -12
  82. package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
  83. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
  84. package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
  85. package/dist/nodefony/src/oauth/providerRoles.js +78 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +48 -12
  87. package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
  88. package/dist/nodefony/src/openToSignup.js +46 -0
  89. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  90. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  91. package/dist/nodefony/src/rolesWithin.js +57 -0
  92. package/dist/nodefony/src/sessionAuthentication.js +25 -0
  93. package/dist/nodefony/src/sessionIdentity.js +29 -3
  94. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  95. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  96. package/dist/nodefony/src/token/JwtKeystore.js +89 -48
  97. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  98. package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
  99. package/dist/nodefony/src/token/UserToken.js +1 -2
  100. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  101. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  102. package/dist/nodefony/src/token/secretFile.js +60 -4
  103. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  104. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  105. package/dist/nodefony/src/token/tokenSort.js +1 -2
  106. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  107. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  108. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  109. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  110. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  111. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  112. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  113. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
  114. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  115. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  116. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  117. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  118. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  119. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  120. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  124. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  125. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  126. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  127. package/dist/types/index.d.ts +26 -6
  128. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
  129. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  130. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  131. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  132. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  133. package/dist/types/nodefony/config/config.d.ts +64 -1
  134. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  135. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  136. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  137. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  138. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  139. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  140. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  141. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  142. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  143. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  144. package/dist/types/nodefony/errors/index.d.ts +1 -0
  145. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  146. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  147. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  148. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  149. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  150. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  151. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  152. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  153. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  154. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  155. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  156. package/dist/types/nodefony/service/oauth2.d.ts +145 -9
  157. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  158. package/dist/types/nodefony/service/totp.d.ts +28 -4
  159. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  160. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  161. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  162. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
  163. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
  164. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  165. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
  166. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  167. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  168. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  169. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  170. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  171. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  172. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  173. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  174. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  175. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  176. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  177. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  178. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  179. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  180. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  181. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  182. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  183. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  184. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  185. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  186. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  187. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  188. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  189. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  190. package/docs/api-keys.md +182 -175
  191. package/docs/audit.md +141 -141
  192. package/docs/authenticators.md +129 -96
  193. package/docs/authorization.md +116 -69
  194. package/docs/cors.md +64 -64
  195. package/docs/csrf.md +77 -72
  196. package/docs/external-jwt.md +83 -11
  197. package/docs/firewall.md +119 -83
  198. package/docs/headers.md +82 -82
  199. package/docs/index.md +16 -8
  200. package/docs/keycloak.md +835 -0
  201. package/docs/login-page.md +361 -0
  202. package/docs/oauth2.md +291 -166
  203. package/docs/obtenir-un-jeton.md +131 -34
  204. package/docs/tokens.md +197 -140
  205. package/docs/totp.md +177 -136
  206. package/docs/webauthn.md +157 -154
  207. package/docs/webhooks.md +128 -125
  208. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  209. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  210. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  211. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  212. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  213. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  214. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  215. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  216. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  217. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  218. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  219. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  220. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  221. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  222. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  223. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  224. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  225. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  226. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  227. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  228. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  229. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  230. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  231. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  232. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  233. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  234. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  235. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  236. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  237. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  238. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  239. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  240. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  241. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  242. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  243. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  244. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  245. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  246. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  247. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  248. package/package.json +19 -17
  249. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,11 +1,57 @@
1
+ import { compileProviderRoleMapping, isPlatformRole, mapProviderRoles } from "../src/oauth/providerRoles.js";
1
2
  import { AuthenticationError } from "../errors/AuthenticationError.js";
2
3
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
4
+ import { recordAudit } from "../src/audit/recordAudit.js";
5
+ import { keycloakLoginThemeCheck } from "../keycloak/keycloakAdmin.js";
3
6
  import { generateCodeVerifier, generateState } from "../src/oauth/oauth2Client.js";
4
- import { getOAuthProviderFactory, listOAuthProviders } from "../src/oauth/oauthProviderRegistry.js";
5
- import { Service } from "nodefony";
6
- //#region nodefony/service/oauth2.ts
7
+ import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer } from "../src/oauth/oauthProviderRegistry.js";
8
+ import { diagnoseOAuthProvider } from "../src/oauth/providerDiagnosis.js";
9
+ import { BootConfigurationError, LOGIN_PAGE_PATH, Service, canonicalIssuer } from "nodefony";
10
+ // nodefony/service/oauth2.ts
7
11
  const serviceName = "oauth2";
8
12
  /**
13
+ * Clé de session de l'indice de déconnexion. Lue et écrite par CE service
14
+ * seulement ({@link OAuth2Service.rememberLogout},
15
+ * {@link OAuth2Service.logoutUrlFor}) : les contrôleurs ne la connaissent pas.
16
+ */
17
+ const LOGOUT_HINT_KEY = "oauth2:logout";
18
+ /**
19
+ * Clé de MÉTADONNÉE de session (`metaBag`) qui indexe un login fédéré :
20
+ * fournisseur et `sid`, sans l'ID token. C'est elle que lit le canal arrière.
21
+ *
22
+ * @remarks Pas dans les attributs : l'énumération des sessions les REDACTE
23
+ * (aucun store ne les rend hors de la session elle-même), quand `metaBag` en
24
+ * sort — et le résumé d'administration ne l'expose que par liste blanche.
25
+ */
26
+ const FEDERATION_KEY = "oauth2:federation";
27
+ /**
28
+ * Plafond de la mémoire anti-rejeu du canal arrière — au-delà, les plus
29
+ * anciens `jti` sont oubliés. Un fournisseur n'émet qu'un jeton par session
30
+ * fermée : ce plafond n'est atteint que sous un flot anormal.
31
+ */
32
+ const MAX_SEEN_LOGOUT_TOKENS = 1e4;
33
+ /** Relit l'index fédéré d'une session énumérée — sa forme se vérifie. */
34
+ function readFederation(value) {
35
+ if (typeof value !== "object" || value === null) return null;
36
+ const { provider, sid } = value;
37
+ if (typeof provider !== "string") return null;
38
+ return {
39
+ provider,
40
+ sid: typeof sid === "string" ? sid : null
41
+ };
42
+ }
43
+ /** Relit un indice de session — il vient du stockage, sa forme se vérifie. */
44
+ function readLogoutHint(value) {
45
+ if (typeof value !== "object" || value === null) return null;
46
+ const { provider, idToken, sid } = value;
47
+ if (typeof provider !== "string" || typeof idToken !== "string") return null;
48
+ return {
49
+ provider,
50
+ idToken,
51
+ sid: typeof sid === "string" ? sid : null
52
+ };
53
+ }
54
+ /**
9
55
  * Sigles qui se lisent en capitales — les capitaliser mot à mot rendrait
10
56
  * « Oidc », « Sso », qu'aucun utilisateur ne reconnaît comme la technologie.
11
57
  */
@@ -58,19 +104,97 @@ function oauthDisplayLabel(name) {
58
104
  return name.split(/[-_.\s]+/).filter((word) => word.length > 0).map((word) => ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
59
105
  }
60
106
  /**
107
+ * Délai avant de retenter un fournisseur dont la construction a échoué — assez
108
+ * long pour ne pas marteler un émetteur en panne à chaque affichage de l'écran
109
+ * de connexion, assez court pour qu'un bouton revienne sans redémarrage.
110
+ */
111
+ const UNREACHABLE_RETRY_MS = 3e4;
112
+ /**
113
+ * Confronte l'émetteur configuré d'un fournisseur à ce que sa fabrique exige.
114
+ *
115
+ * Une URL qui ne peut pas être un émetteur est refusée QUEL QUE SOIT le
116
+ * fournisseur : aucune valeur mal formée n'est une configuration voulue. Une
117
+ * absence ne l'est que si la fabrique a déclaré l'émetteur requis.
118
+ *
119
+ * @param name - nom du fournisseur (`oauth2.providers.<name>`).
120
+ * @param issuer - émetteur lu dans sa configuration.
121
+ * @throws BootConfigurationError - émetteur requis absent, ou mal formé.
122
+ */
123
+ function checkProviderIssuer(name, issuer) {
124
+ const key = `security.oauth2.providers.${name}.issuer`;
125
+ if (issuer === void 0 || issuer === "") {
126
+ if (oauthProviderRequiresIssuer(name)) throw new BootConfigurationError(`[@nodefony/security] ${key} est requis : le fournisseur « ${name} » découvre ses points d'entrée à partir de son émetteur. Attendu : l'URL https de l'émetteur OpenID Connect (Keycloak : URL du realm, ex. « https://auth.example.com/realms/mon-royaume »).`);
127
+ return;
128
+ }
129
+ try {
130
+ canonicalIssuer(issuer);
131
+ } catch (error) {
132
+ throw new BootConfigurationError(`[@nodefony/security] ${key} — ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ }
135
+ /**
136
+ * Annonce qu'un annuaire accorde des rôles de PLATEFORME, à la sévérité de
137
+ * l'environnement — ou `null` quand il n'y a rien à dire.
138
+ *
139
+ * L'ouverture est ÉCRITE (`allowPlatformRoles`), donc voulue : l'annonce ne
140
+ * la conteste pas, elle la rappelle. Le risque — un annuaire compromis
141
+ * administre l'instance — n'est réel qu'en production : là, `WARNING` ;
142
+ * ailleurs le même fait est dit en `INFO`, sans sonner une alarme à chaque
143
+ * démarrage d'un décor de développement.
144
+ *
145
+ * @param name - nom du fournisseur (`oauth2.providers.<name>`).
146
+ * @param allowed - `allowPlatformRoles` tel que déclaré.
147
+ * @param granted - rôles de l'application que la table accorde.
148
+ * @param production - l'application tourne en production.
149
+ * @returns le message et sa sévérité, ou `null`.
150
+ */
151
+ function platformRolesNotice(name, allowed, granted, production) {
152
+ if (allowed !== true) return null;
153
+ const platform = [...new Set(granted.filter(isPlatformRole))];
154
+ if (platform.length === 0) return null;
155
+ return {
156
+ message: `oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${platform.join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`,
157
+ severity: production ? "WARNING" : "INFO"
158
+ };
159
+ }
160
+ /**
161
+ * Claims du jeton d'ACCÈS, quand c'est un JWT — `null` sinon (jeton opaque).
162
+ *
163
+ * Keycloak pose les rôles dans le jeton d'accès, et seulement là par défaut
164
+ * (`realm_access`, `resource_access`) : ni le jeton d'identité ni userinfo ne
165
+ * les portent sans « mapper » ajouté. La signature n'est pas vérifiée, pour la
166
+ * raison qui vaut pour le jeton d'identité (OpenID Connect Core §3.1.3.7) : il
167
+ * vient d'être reçu du point de jeton, sur un canal TLS direct et authentifié.
168
+ */
169
+ async function accessTokenClaims(tokens) {
170
+ try {
171
+ return (await import("jose")).decodeJwt(tokens.accessToken());
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ /**
61
177
  * **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*.
62
178
  *
63
179
  * Posture OAuth 2.1 (RFC 9700) : Authorization Code uniquement (jamais implicit /
64
180
  * ROPC), **PKCE S256** quand le fournisseur le supporte (RFC 7636), **state**
65
- * anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton n'atteint le navigateur
66
- * (le login produit une **session BFF**, gérée hors de ce service par le
67
- * controller + `AuthFlow`).
181
+ * anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton d'accès n'atteint le
182
+ * navigateur (le login produit une **session BFF**, gérée hors de ce service par
183
+ * le controller + `AuthFlow`) — seul l'ID token y passe, une fois, à la
184
+ * déconnexion ({@link IFederatedLogoutHint}).
68
185
  *
69
- * Les fournisseurs sont construits **au premier login** (cold path — jamais au boot
70
- * ni par requête) puis mémoïsés : c'est là que les points d'entrée d'un émetteur
71
- * OIDC sont découverts, une seule fois par processus. Au boot (si `oauth2.enabled`)
72
- * : seule la config est validée et les fournisseurs configurés sont confrontés au
73
- * registre (un nom inconnu = WARNING, pas fatal).
186
+ * Au boot (si `oauth2.enabled`), la configuration de chaque fournisseur est
187
+ * confrontée à ce que sa fabrique exige : un émetteur requis absent, ou qui n'est
188
+ * pas une URL d'émetteur valide, INTERROMPT le démarrage en nommant la clé — une
189
+ * faute d'écriture ne doit pas attendre le premier clic pour se montrer. Un nom
190
+ * inconnu du registre reste un WARNING.
191
+ *
192
+ * Les fournisseurs sont construits une fois puis mémoïsés : c'est là que les
193
+ * points d'entrée d'un émetteur OIDC sont découverts. Un run qui sert (ports
194
+ * ouverts) les construit DÈS le boot, sans l'attendre : un émetteur injoignable
195
+ * n'est pas une faute de configuration, il se journalise et son bouton quitte
196
+ * l'écran de connexion jusqu'à ce qu'une nouvelle tentative réussisse. Un run
197
+ * console ne touche jamais le réseau.
74
198
  *
75
199
  * Le service ne touche **ni HTTP ni session** : il rend à l'appelant les éléments
76
200
  * (URL, state, verifier) que le controller persiste en session — testable sans
@@ -80,11 +204,15 @@ var OAuth2Service = class extends Service {
80
204
  module;
81
205
  #config = null;
82
206
  #providers = null;
207
+ #unreachable = null;
208
+ #seenLogoutTokens = null;
209
+ #roleMappings = null;
83
210
  #ready = false;
84
211
  constructor(module) {
85
212
  super(serviceName, module.container, module.notificationsCenter, module.options);
86
213
  this.module = module;
87
- this.kernel?.once("onBoot", () => this.#build());
214
+ const oauth2ConfigCheck = () => this.#build();
215
+ this.kernel?.once("onBoot", oauth2ConfigCheck);
88
216
  }
89
217
  #build() {
90
218
  let config;
@@ -97,13 +225,32 @@ var OAuth2Service = class extends Service {
97
225
  this.log("oauth2 idle — social login désactivé en config", "DEBUG");
98
226
  return;
99
227
  }
100
- this.#config = config;
101
228
  const known = new Set(listOAuthProviders());
102
229
  const configured = Object.keys(config.oauth2.providers);
103
230
  for (const name of configured) if (!known.has(name)) this.log(`oauth2 provider "${name}" inconnu du registre — ignoré (registerOAuthProvider manquant ?)`, "WARNING");
104
- this.#ready = true;
105
231
  const active = configured.filter((n) => known.has(n));
232
+ for (const name of active) {
233
+ const provider = config.oauth2.providers[name];
234
+ checkProviderIssuer(name, provider?.issuer);
235
+ const mapping = provider === void 0 ? null : compileProviderRoleMapping(provider);
236
+ if (mapping !== null) {
237
+ const notice = platformRolesNotice(name, provider?.allowPlatformRoles, Object.values(mapping.table), this.kernel?.environment === "production");
238
+ if (notice !== null) this.log(notice.message, notice.severity);
239
+ this.#roleMappings ??= Object.create(null);
240
+ this.#roleMappings[name] = mapping;
241
+ }
242
+ }
243
+ this.#config = config;
244
+ this.#ready = true;
106
245
  this.log(`oauth2 ready — providers: [${active.join(", ") || "aucun"}]`, "DEBUG");
246
+ if (this.kernel?.runProfile.servers === true) for (const name of active) this.#probe(name);
247
+ }
248
+ /**
249
+ * Construit un fournisseur sans attendre le résultat — l'échec est consigné
250
+ * par {@link #resolveProvider}, rien n'est levé vers l'appelant.
251
+ */
252
+ #probe(name) {
253
+ this.#resolveProvider(name).catch(() => void 0);
107
254
  }
108
255
  /** `true` si le social login est opérationnel (activé + boot OK). */
109
256
  isEnabled() {
@@ -135,30 +282,43 @@ var OAuth2Service = class extends Service {
135
282
  listDisplayProviders() {
136
283
  if (this.#config === null) return [];
137
284
  const configured = this.#config.oauth2.providers;
138
- return this.listProviders().filter((name) => !configured[name]?.hidden).map((name) => ({
139
- name,
140
- label: configured[name]?.label ?? oauthDisplayLabel(name)
141
- }));
285
+ return this.listProviders().filter((name) => !configured[name]?.hidden).filter((name) => this.#isReachable(name)).map((name) => {
286
+ const label = configured[name]?.label ?? oauthDisplayLabel(name);
287
+ const icon = configured[name]?.icon;
288
+ return icon === void 0 ? {
289
+ name,
290
+ label
291
+ } : {
292
+ name,
293
+ label,
294
+ icon
295
+ };
296
+ });
142
297
  }
143
298
  /**
144
299
  * Redirections post-login (succès / échec) — lues par le controller.
145
300
  * Surcharge PAR FOURNISSEUR si fournie, sinon valeur globale, sinon défaut.
301
+ *
302
+ * @remarks L'échec mène par défaut à la page de connexion (`loginPage.path`) :
303
+ * déplacer la page déplace la redirection, les deux ne peuvent pas diverger.
146
304
  */
147
305
  getRedirects(provider) {
148
306
  const o = this.#config?.oauth2;
149
307
  const p = provider ? o?.providers[provider] : void 0;
150
308
  return {
151
309
  success: p?.successRedirect ?? o?.successRedirect ?? "/",
152
- failure: p?.failureRedirect ?? o?.failureRedirect ?? "/login"
310
+ failure: p?.failureRedirect ?? o?.failureRedirect ?? this.#config?.loginPage.path ?? LOGIN_PAGE_PATH
153
311
  };
154
312
  }
155
313
  /**
156
314
  * Étape 1 — prépare l'URL d'autorisation + les éléments anti-replay à stocker
157
315
  * en session (`state`, et `code_verifier` si PKCE).
158
316
  *
317
+ * @param options.theme - thème rendu par l'application, transmis au seul
318
+ * fournisseur qui sait l'afficher (Keycloak) ; les autres l'ignorent
159
319
  * @throws AuthenticationError — fournisseur non configuré / inconnu du registre.
160
320
  */
161
- async createAuthorization(provider) {
321
+ async createAuthorization(provider, options = {}) {
162
322
  const resolved = await this.#resolveProvider(provider);
163
323
  const state = generateState();
164
324
  const codeVerifier = resolved.provider.usesPkce ? generateCodeVerifier() : null;
@@ -166,7 +326,8 @@ var OAuth2Service = class extends Service {
166
326
  url: resolved.provider.createAuthorizationURL({
167
327
  state,
168
328
  codeVerifier,
169
- scopes: resolved.scopes
329
+ scopes: resolved.scopes,
330
+ ...options.theme ? { theme: options.theme } : {}
170
331
  }).toString(),
171
332
  state,
172
333
  codeVerifier
@@ -194,10 +355,202 @@ var OAuth2Service = class extends Service {
194
355
  const profile = await p.fetchProfile(tokens);
195
356
  const cfg = this.#ensureReady().oauth2;
196
357
  const defaultRoles = cfg.providers[provider]?.defaultRoles ?? cfg.defaultRoles;
197
- return { identifier: (await this.#resolveProvisioner().provisionOAuthUser(profile, {
358
+ const mapping = this.#roleMappings?.[provider];
359
+ const providerRoles = mapping === void 0 ? void 0 : mapProviderRoles(mapping, await accessTokenClaims(tokens), profile.raw);
360
+ const user = await this.#resolveProvisioner().provisionOAuthUser(profile, {
198
361
  defaultRoles: [...defaultRoles],
199
- allowSignup: cfg.allowSignup
200
- })).identifier };
362
+ allowSignup: cfg.allowSignup,
363
+ ...providerRoles === void 0 ? {} : { providerRoles }
364
+ });
365
+ let logoutHint = null;
366
+ if (p.createLogoutURL !== void 0) {
367
+ const sid = profile.raw?.sid;
368
+ logoutHint = {
369
+ provider,
370
+ idToken: tokens.idToken(),
371
+ sid: typeof sid === "string" ? sid : null
372
+ };
373
+ }
374
+ return {
375
+ identifier: user.identifier,
376
+ logoutHint
377
+ };
378
+ }
379
+ /**
380
+ * Retient en session l'indice de déconnexion d'un login fédéré — à appeler
381
+ * APRÈS l'ouverture de la session authentifiée (l'ID de session est
382
+ * régénéré à l'ouverture).
383
+ *
384
+ * @param session - session authentifiée de la requête.
385
+ * @param hint - indice rendu par {@link exchangeAndProvision}, ou `null`.
386
+ */
387
+ rememberLogout(session, hint) {
388
+ if (hint === null) return;
389
+ session.set(LOGOUT_HINT_KEY, hint);
390
+ session.setMetaBag?.(FEDERATION_KEY, {
391
+ provider: hint.provider,
392
+ sid: hint.sid
393
+ });
394
+ }
395
+ /**
396
+ * Adresse de déconnexion chez le fournisseur qui a ouvert la session, ou
397
+ * `null` : session locale, fournisseur sans point de déconnexion, ou
398
+ * fournisseur devenu indisponible. À lire AVANT de détruire la session.
399
+ *
400
+ * @param session - session courante, ou `null`.
401
+ * @returns l'URL vers laquelle envoyer le navigateur, ou `null`.
402
+ */
403
+ async logoutUrlFor(session) {
404
+ const hint = readLogoutHint(session?.get(LOGOUT_HINT_KEY));
405
+ if (hint === null || !this.#ready) return null;
406
+ let p;
407
+ try {
408
+ ({provider: p} = await this.#resolveProvider(hint.provider));
409
+ } catch {
410
+ return null;
411
+ }
412
+ if (p.createLogoutURL === void 0) return null;
413
+ return p.createLogoutURL({
414
+ idTokenHint: hint.idToken,
415
+ postLogoutRedirectUri: this.#postLogoutRedirectUri(hint.provider)
416
+ }).toString();
417
+ }
418
+ /**
419
+ * Déconnexion demandée PAR le fournisseur sur le canal arrière (OpenID
420
+ * Connect Back-Channel Logout 1.0) : vérifie le jeton, refuse un rejeu, puis
421
+ * détruit les sessions locales qu'il désigne.
422
+ *
423
+ * Une session est désignée si elle a été ouverte par CE fournisseur (index
424
+ * fédéré posé au login par {@link rememberLogout}) et, selon le jeton : porte son `sid` ; ou,
425
+ * sans `sid`, appartient à l'utilisateur lié à son `sub`. Une session ouverte
426
+ * par mot de passe ou par un autre fournisseur n'est jamais touchée — le
427
+ * fournisseur ne ferme que ce qu'il a ouvert.
428
+ *
429
+ * @remarks Les connexions WebSocket portées par une session détruite se
430
+ * ferment au tick de revalidation du hub temps réel (code `4001`).
431
+ *
432
+ * @param provider - fournisseur désigné par l'adresse appelée.
433
+ * @param logoutToken - paramètre `logout_token` reçu, brut.
434
+ * @returns `unsupported` si le fournisseur ne sait pas émettre de jeton de
435
+ * déconnexion ; `refused` si le jeton est invalide ou rejoué ; sinon le
436
+ * nombre de sessions détruites (0 : déjà déconnecté, c'est un succès §2.7).
437
+ * @throws Error - vérification impossible (jeu de clés injoignable) ou
438
+ * sessions impossibles à parcourir : la déconnexion a ÉCHOUÉ.
439
+ */
440
+ async backchannelLogout(provider, logoutToken) {
441
+ const { provider: p } = await this.#resolveProvider(provider);
442
+ if (p.verifyLogoutToken === void 0) return { outcome: "unsupported" };
443
+ const claims = await p.verifyLogoutToken(logoutToken);
444
+ if (claims === null || !this.#firstSeen(provider, claims)) return { outcome: "refused" };
445
+ let owner = null;
446
+ if (claims.subject !== null) {
447
+ const users = this.get("users");
448
+ try {
449
+ owner = (await users?.loadUserByOAuth?.(provider, claims.subject))?.identifier ?? null;
450
+ } catch {
451
+ owner = null;
452
+ }
453
+ if (owner === null && claims.sid === null) return {
454
+ outcome: "done",
455
+ destroyed: 0
456
+ };
457
+ }
458
+ const sessions = this.get("sessions");
459
+ if (sessions?.destroyWhere === void 0) throw new Error(`aucun service "sessions" capable de révocation groupée (destroyWhere)`);
460
+ const destroyed = await sessions.destroyWhere(owner !== null ? { user: owner } : void 0, (data) => {
461
+ if (owner !== null && data.user !== owner) return false;
462
+ const federation = readFederation(data.metaBag?.[FEDERATION_KEY]);
463
+ if (federation === null || federation.provider !== provider) return false;
464
+ return claims.sid === null || federation.sid === claims.sid;
465
+ });
466
+ this.log(`oauth2 back-channel logout "${provider}" — ${destroyed} session(s) fermée(s)` + (owner !== null ? ` user=${owner}` : ""), "INFO");
467
+ if (destroyed > 0) recordAudit(this.container, {
468
+ category: "session",
469
+ action: "session.revoked",
470
+ outcome: "success",
471
+ actor: `oauth2:${provider}`,
472
+ resource: owner,
473
+ reason: "backchannel_logout",
474
+ metadata: {
475
+ count: destroyed,
476
+ bySid: claims.sid !== null
477
+ }
478
+ });
479
+ return {
480
+ outcome: "done",
481
+ destroyed
482
+ };
483
+ }
484
+ /**
485
+ * Anti-rejeu du canal arrière (§2.6, point 8) : `true` au premier passage
486
+ * d'un `jti`, `false` s'il a déjà été vu et n'a pas expiré.
487
+ *
488
+ * @remarks Mémoire PAR PROCESSUS, bornée : le même jeton rejoué sur un autre
489
+ * exemplaire passerait. Le rejeu n'y fermerait que des sessions que le
490
+ * fournisseur a déjà déclarées closes — la borne est la durée de vie du
491
+ * jeton, que le fournisseur garde courte (§4).
492
+ */
493
+ #firstSeen(provider, claims) {
494
+ const now = Date.now();
495
+ const seen = this.#seenLogoutTokens ??= /* @__PURE__ */ new Map();
496
+ for (const [key, expiresAt] of seen) {
497
+ if (expiresAt > now && seen.size < MAX_SEEN_LOGOUT_TOKENS) break;
498
+ seen.delete(key);
499
+ }
500
+ const key = `${provider}\n${claims.tokenId}`;
501
+ const known = seen.get(key);
502
+ if (known !== void 0 && known > now) return false;
503
+ seen.set(key, claims.expiresAt * 1e3);
504
+ return true;
505
+ }
506
+ /**
507
+ * Retour après déconnexion chez le fournisseur : la valeur configurée, sinon
508
+ * l'écran d'échec (la page de connexion) résolu contre `redirectUri` — une
509
+ * adresse ABSOLUE, sur une origine déjà enregistrée chez le fournisseur.
510
+ *
511
+ * @remarks Query et fragment de l'écran d'échec sont RETIRÉS : ils signalent
512
+ * un échec (`?error=oauth`), et un fournisseur OpenID Connect refuse une
513
+ * adresse de retour qui porte un paramètre du protocole (`error`, `code`…) —
514
+ * Keycloak répond `invalid_redirect_uri`.
515
+ */
516
+ #postLogoutRedirectUri(provider) {
517
+ const cfg = this.#ensureReady().oauth2.providers[provider];
518
+ if (cfg?.postLogoutRedirectUri !== void 0) return cfg.postLogoutRedirectUri;
519
+ const { failure } = this.getRedirects(provider);
520
+ const url = new URL(failure, cfg?.redirectUri);
521
+ url.search = "";
522
+ url.hash = "";
523
+ return url.toString();
524
+ }
525
+ /**
526
+ * Diagnostique les fournisseurs CONFIGURÉS sans connexion humaine : découverte
527
+ * et émetteur, URL de retour acceptée, secret accepté (cf
528
+ * `diagnoseOAuthProvider`).
529
+ *
530
+ * Le fournisseur est construit à NEUF, hors de la mémoïsation du login : un
531
+ * diagnostic doit voir le serveur tel qu'il est maintenant, et son échec ne
532
+ * doit pas retirer un bouton de l'écran de connexion.
533
+ *
534
+ * @param name - un seul fournisseur ; à défaut, tous ceux de la configuration.
535
+ * @returns un diagnostic par fournisseur ; vide si le social login est éteint.
536
+ */
537
+ async diagnose(name) {
538
+ if (!this.#ready || this.#config === null) return [];
539
+ const providers = this.#config.oauth2.providers;
540
+ const names = name === void 0 ? Object.keys(providers) : [name];
541
+ const pageCheck = keycloakLoginThemeCheck(this.#config.loginPage.skin);
542
+ return Promise.all(names.map((n) => {
543
+ const cfg = Object.hasOwn(providers, n) ? providers[n] : void 0;
544
+ return diagnoseOAuthProvider({
545
+ name: n,
546
+ build: async () => (await this.#buildProvider(n)).provider,
547
+ clientId: cfg?.clientId ?? "",
548
+ redirectUri: cfg?.redirectUri ?? "",
549
+ clientAuthMethod: cfg?.clientAuthMethod,
550
+ scopes: cfg?.scopes,
551
+ pageCheck
552
+ });
553
+ }));
201
554
  }
202
555
  #resolveProvider(name) {
203
556
  this.#ensureReady();
@@ -206,9 +559,31 @@ var OAuth2Service = class extends Service {
206
559
  if (cached) return cached;
207
560
  const pending = this.#buildProvider(name);
208
561
  this.#providers.set(name, pending);
209
- pending.catch(() => this.#providers?.delete(name));
562
+ pending.then(() => {
563
+ if (this.#unreachable !== null && name in this.#unreachable) {
564
+ delete this.#unreachable[name];
565
+ this.log(`oauth2 provider "${name}" de nouveau joignable`, "INFO");
566
+ }
567
+ }, (error) => {
568
+ this.#providers?.delete(name);
569
+ this.#unreachable ??= Object.create(null);
570
+ this.#unreachable[name] = Date.now();
571
+ this.log(`oauth2 provider "${name}" indisponible — bouton retiré de l'écran de connexion : ${error instanceof Error ? error.message : String(error)}`, "WARNING");
572
+ });
210
573
  return pending;
211
574
  }
575
+ /**
576
+ * `true` si le fournisseur n'a pas échoué récemment. Un échec plus ancien que
577
+ * {@link UNREACHABLE_RETRY_MS} relance une construction en arrière-plan : sans
578
+ * elle, un bouton retiré ne reviendrait jamais, puisque plus personne ne
579
+ * pourrait cliquer pour déclencher la tentative suivante.
580
+ */
581
+ #isReachable(name) {
582
+ const failedAt = this.#unreachable?.[name];
583
+ if (failedAt === void 0) return true;
584
+ if (Date.now() - failedAt >= UNREACHABLE_RETRY_MS && !this.#providers?.has(name)) this.#probe(name);
585
+ return false;
586
+ }
212
587
  async #buildProvider(name) {
213
588
  const providers = this.#ensureReady().oauth2.providers;
214
589
  const cfg = Object.hasOwn(providers, name) ? providers[name] : void 0;
@@ -220,7 +595,8 @@ var OAuth2Service = class extends Service {
220
595
  clientSecret: cfg.clientSecret,
221
596
  clientAuthMethod: cfg.clientAuthMethod,
222
597
  redirectUri: cfg.redirectUri,
223
- issuer: cfg.issuer
598
+ issuer: cfg.issuer,
599
+ discoveryUrl: cfg.discoveryUrl
224
600
  });
225
601
  return {
226
602
  provider,
@@ -237,5 +613,4 @@ var OAuth2Service = class extends Service {
237
613
  return this.#config;
238
614
  }
239
615
  };
240
- //#endregion
241
- export { OAuth2Service, OAuth2Service as default, oauthDisplayLabel };
616
+ export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel, platformRolesNotice };
@@ -1,5 +1,5 @@
1
1
  import { mergeCspFragments } from "../src/csp.js";
2
- //#region nodefony/service/securityHeaders.ts
2
+ // nodefony/service/securityHeaders.ts
3
3
  /** Marqueur substitué par le nonce CSP de la requête (cf `Context.cspNonce`). */
4
4
  const NONCE_PLACEHOLDER = "{{nonce}}";
5
5
  /**
@@ -87,5 +87,4 @@ var SecurityHeaders = class {
87
87
  return this.#cspParts !== null ? merged.split(NONCE_PLACEHOLDER).join(nonce) : merged.replace(/\s*'nonce-\{\{nonce\}\}'/g, "");
88
88
  }
89
89
  };
90
- //#endregion
91
90
  export { SecurityHeaders, SecurityHeaders as default };