@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,8 +1,8 @@
1
1
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
2
  import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
3
- import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
3
+ import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
4
4
  import { randomBytes } from "node:crypto";
5
- //#region nodefony/service/auditService.ts
5
+ // nodefony/service/auditService.ts
6
6
  const serviceName = "auditService";
7
7
  const GC_INTERVAL_MS = 36e5;
8
8
  /**
@@ -50,7 +50,7 @@ var AuditService = class extends Service {
50
50
  let storeName = config.audit.store;
51
51
  let reason = `store explicitement configuré ("${storeName}")`;
52
52
  if (storeName === AUTO_STORE) {
53
- const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", runNeedsExternalServices(this.kernel));
53
+ const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", storeConnectivity(this.kernel));
54
54
  storeName = auto.store;
55
55
  reason = auto.reason;
56
56
  this.log(`audit.store "auto" → "${storeName}" (${auto.reason})`, "INFO");
@@ -62,7 +62,7 @@ var AuditService = class extends Service {
62
62
  this.log(`${msg} — audit désactivé (journal de sécurité non collecté)`, "CRITIC");
63
63
  return;
64
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. " + durableStoreRemedy(runNeedsExternalServices(this.kernel)), "WARNING");
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. " + durableStoreRemedy(storeConnectivity(this.kernel)), "WARNING");
66
66
  this.#enabled = true;
67
67
  this.#idPrefix = randomBytes(4).toString("hex");
68
68
  this.#store = factory({
@@ -142,5 +142,4 @@ var AuditService = class extends Service {
142
142
  return this.#enabled;
143
143
  }
144
144
  };
145
- //#endregion
146
145
  export { AuditService as default };
@@ -1,10 +1,16 @@
1
+ import { securityConfigSchema } from "../config/config.js";
2
+ import { InsecureTransportError } from "../errors/InsecureTransportError.js";
1
3
  import { AuthenticationError } from "../errors/AuthenticationError.js";
2
4
  import { ThrottledError } from "../errors/ThrottledError.js";
3
- import { resolveSessionIdentity } from "../src/sessionIdentity.js";
5
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
6
+ import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
4
7
  import { recordAudit } from "../src/audit/recordAudit.js";
5
8
  import { readAuditContext } from "../src/audit/readAuditContext.js";
6
- import { RequestContext, Service } from "nodefony";
7
- //#region nodefony/service/authFlow.ts
9
+ import { amrForFactor } from "../src/sessionAuthentication.js";
10
+ import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
11
+ import { requestUser } from "@nodefony/user";
12
+ // nodefony/service/authFlow.ts
13
+ const MFA_METHODS = Object.freeze([MFA_METHOD_TOTP]);
8
14
  const serviceName = "authFlow";
9
15
  const INVALID_CREDENTIALS = "Invalid credentials";
10
16
  const PENDING_MFA_KEY = "mfa:pending";
@@ -34,8 +40,11 @@ var AuthFlow = class extends Service {
34
40
  #sessions = null;
35
41
  #throttler = null;
36
42
  #throttlerResolved = false;
43
+ #transport = null;
37
44
  #totp = null;
38
45
  #totpResolved = false;
46
+ #loginPage = null;
47
+ #loginPageResolved = false;
39
48
  constructor(module) {
40
49
  super(serviceName, module.container, module.notificationsCenter, module.options);
41
50
  this.module = module;
@@ -51,6 +60,8 @@ var AuthFlow = class extends Service {
51
60
  * @param identifier - identifiant saisi (body JSON, non typé à la frontière).
52
61
  * @param password - mot de passe saisi.
53
62
  * @returns `authenticated` (identité établie) ou `mfa_required` (2ᵉ facteur requis).
63
+ * @throws InsecureTransportError (403) — mot de passe reçu en clair en
64
+ * production (refusé AVANT le throttle et le verifier).
54
65
  * @throws ThrottledError (429 + `Retry-After`) — backoff actif.
55
66
  * @throws AuthenticationError (401, message uniforme) — credential absent ou
56
67
  * invalide.
@@ -58,6 +69,7 @@ var AuthFlow = class extends Service {
58
69
  async login(context, identifier, password) {
59
70
  const info = readAuditContext(context);
60
71
  const who = typeof identifier === "string" ? identifier : null;
72
+ this.#assertTransport(context, "login", who, info);
61
73
  if (typeof identifier !== "string" || identifier.length === 0 || typeof password !== "string" || password.length === 0) {
62
74
  recordAudit(this.container, {
63
75
  category: "auth",
@@ -113,12 +125,10 @@ var AuthFlow = class extends Service {
113
125
  });
114
126
  return {
115
127
  status: "mfa_required",
116
- methods: ["totp"]
128
+ methods: MFA_METHODS
117
129
  };
118
130
  }
119
- await this.#openSession(context, user.identifier);
120
- context.user = user.identifier;
121
- RequestContext.set("user", user);
131
+ await this.#openSession(context, user, "password");
122
132
  recordAudit(this.container, {
123
133
  category: "auth",
124
134
  action: "login.success",
@@ -148,9 +158,7 @@ var AuthFlow = class extends Service {
148
158
  async establishSessionFor(context, identifier, reason = "federated") {
149
159
  if (typeof identifier !== "string" || identifier.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
150
160
  const user = await resolveSessionIdentity(this.#resolveUsers(), identifier);
151
- await this.#openSession(context, user.identifier);
152
- context.user = user.identifier;
153
- RequestContext.set("user", user);
161
+ await this.#openSession(context, user, reason);
154
162
  recordAudit(this.container, {
155
163
  category: "auth",
156
164
  action: "login.success",
@@ -171,6 +179,7 @@ var AuthFlow = class extends Service {
171
179
  * @param context - contexte HTTP (porte la session PENDING).
172
180
  * @param code - code présenté (TOTP ou code de récupération).
173
181
  * @returns la projection publique de l'utilisateur authentifié.
182
+ * @throws InsecureTransportError (403) — code reçu en clair en production.
174
183
  * @throws ThrottledError (429) — trop de tentatives.
175
184
  * @throws AuthenticationError (401, uniforme) — aucun défi en cours, code absent
176
185
  * ou invalide (la session N'est PAS ouverte ; le défi reste pour un retry).
@@ -178,6 +187,7 @@ var AuthFlow = class extends Service {
178
187
  async completeMfaLogin(context, code) {
179
188
  const info = readAuditContext(context);
180
189
  const pending = context.session?.get(PENDING_MFA_KEY);
190
+ this.#assertTransport(context, "login/totp", typeof pending === "string" && pending.length > 0 ? pending : null, info);
181
191
  if (typeof pending !== "string" || pending.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
182
192
  const throttler = this.#resolveThrottler();
183
193
  if (throttler !== null) {
@@ -253,23 +263,95 @@ var AuthFlow = class extends Service {
253
263
  return true;
254
264
  }
255
265
  /**
266
+ * Déconnexion COMPLÈTE : détruit la session locale et rend, si la session
267
+ * vient d'un fournisseur qui publie un point de déconnexion (Keycloak…),
268
+ * l'adresse où envoyer le navigateur pour fermer AUSSI sa session à lui —
269
+ * sans quoi le clic suivant sur « Continuer avec … » reconnecte sans mot de
270
+ * passe (OpenID Connect RP-Initiated Logout 1.0).
271
+ *
272
+ * @returns `destroyed` comme {@link logout} ; `logoutUrl` à suivre, ou `null`.
273
+ */
274
+ async logoutWithProvider(context) {
275
+ const session = context.session;
276
+ const logoutUrl = session?.status === "active" ? await this.get("oauth2")?.logoutUrlFor(session) ?? null : null;
277
+ return {
278
+ destroyed: await this.logout(context),
279
+ logoutUrl
280
+ };
281
+ }
282
+ /**
256
283
  * Identité portée par la session courante, re-résolue auprès du provider
257
284
  * (mêmes contrôles que le `SessionAuthenticator` — source unique).
258
285
  *
259
286
  * @returns la projection publique, ou `null` (pas de session, session
260
- * orpheline, compte verrouillé/désactivé) — le handler répond 401.
287
+ * orpheline, compte verrouillé/désactivé) — le handler répond `200`
288
+ * `{ user: null }`.
261
289
  */
262
290
  async me(context) {
263
291
  const identifier = context.session?.user;
264
292
  if (typeof identifier !== "string" || identifier.length === 0) return null;
293
+ const current = requestUser(identifier);
294
+ if (current !== null) return toSafeUser(current);
265
295
  try {
266
- return toSafeUser(await resolveSessionIdentity(this.#resolveUsers(), identifier));
296
+ return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
267
297
  } catch (error) {
268
298
  if (error instanceof AuthenticationError) return null;
269
299
  throw error;
270
300
  }
271
301
  }
272
- async #openSession(context, identifier) {
302
+ /**
303
+ * Décrit la page de connexion par défaut : ce qu'elle propose et comment
304
+ * elle s'habille, depuis `security.loginPage` et les fournisseurs
305
+ * opérationnels.
306
+ *
307
+ * Lue par le contrôleur de `@nodefony/framework`, qui n'importe pas ce
308
+ * paquet. Les fournisseurs sont relus à chaque appel : l'un d'eux peut
309
+ * tomber ou revenir pendant que le serveur tourne.
310
+ *
311
+ * @returns la description, ou `null` quand la page par défaut est
312
+ * désactivée (`loginPage.enabled: false`) ou la configuration invalide —
313
+ * le contrôleur répond alors 404.
314
+ */
315
+ describeLoginPage() {
316
+ const page = this.#resolveLoginPage();
317
+ if (page === null || !page.enabled) return null;
318
+ const offered = this.get("oauth2")?.listDisplayProviders() ?? [];
319
+ return {
320
+ path: page.path,
321
+ title: page.title ?? null,
322
+ logo: page.logo ?? null,
323
+ template: page.template ?? null,
324
+ heading: page.heading ?? null,
325
+ subtitle: page.subtitle ?? null,
326
+ stylesheet: page.stylesheet ?? null,
327
+ providersFirst: page.providersFirst,
328
+ hero: page.hero === void 0 ? null : page.hero === false ? false : {
329
+ heading: page.hero.heading,
330
+ text: page.hero.text ?? null
331
+ },
332
+ footer: page.footer,
333
+ skin: page.skin,
334
+ layout: page.layout ?? LOGIN_PAGE_SKIN_SPECS[page.skin].layout,
335
+ previewPicker: page.previewPicker,
336
+ previewHook: page.previewHook ?? null,
337
+ password: page.password,
338
+ passkey: this.get("webauthn")?.isEnabled() ?? false,
339
+ providers: offered.map(({ name, label, icon }) => ({
340
+ name,
341
+ label,
342
+ icon: icon ?? null
343
+ }))
344
+ };
345
+ }
346
+ #resolveLoginPage() {
347
+ if (!this.#loginPageResolved) {
348
+ this.#loginPageResolved = true;
349
+ const parsed = securityConfigSchema.safeParse(this.options);
350
+ this.#loginPage = parsed.success ? parsed.data.loginPage : null;
351
+ }
352
+ return this.#loginPage;
353
+ }
354
+ async #openSession(context, user, factor) {
273
355
  const session = context.session ?? await this.#resolveSessions().start(context);
274
356
  if (session === null) {
275
357
  this.log("login: session service returned no session", "ERROR");
@@ -287,7 +369,11 @@ var AuthFlow = class extends Service {
287
369
  const ua = provenance.getUserAgent?.();
288
370
  if (ua) session.setMetaBag("ua", ua);
289
371
  } catch {}
290
- await session.save(identifier);
372
+ session.setMetaBag(SESSION_AUTH_AT_KEY, Date.now());
373
+ session.setMetaBag(SESSION_AMR_KEY, amrForFactor(factor));
374
+ session.setMetaBag(ACCOUNT_ID_KEY, user.id);
375
+ session.setMetaBag(CREDENTIALS_VERSION_KEY, credentialsVersionOf(user));
376
+ await session.save(user.identifier);
291
377
  }
292
378
  #resolveSessions() {
293
379
  if (this.#sessions === null) {
@@ -305,6 +391,23 @@ var AuthFlow = class extends Service {
305
391
  }
306
392
  return this.#users;
307
393
  }
394
+ #assertTransport(context, door, actor, info) {
395
+ this.#transport ??= this.get("credentialTransport") ?? null;
396
+ if (this.#transport === null) return;
397
+ try {
398
+ this.#transport.assert(context, door);
399
+ } catch (error) {
400
+ if (error instanceof InsecureTransportError) recordAudit(this.container, {
401
+ category: "auth",
402
+ action: "login.failure",
403
+ outcome: "failure",
404
+ actor,
405
+ reason: "insecure_transport",
406
+ ...info
407
+ });
408
+ throw error;
409
+ }
410
+ }
308
411
  #resolveThrottler() {
309
412
  if (!this.#throttlerResolved) {
310
413
  this.#throttler = this.get("loginThrottler") ?? null;
@@ -324,9 +427,8 @@ var AuthFlow = class extends Service {
324
427
  function toSafeUser(user) {
325
428
  return {
326
429
  id: user.id,
327
- username: user.identifier,
430
+ identifier: user.identifier,
328
431
  roles: [...user.roles]
329
432
  };
330
433
  }
331
- //#endregion
332
434
  export { AuthFlow, AuthFlow as default };
@@ -2,7 +2,7 @@ import { recordAudit } from "../src/audit/recordAudit.js";
2
2
  import "../contracts/IAccessVoter.js";
3
3
  import { listVoterFactories } from "../src/voter/voterRegistry.js";
4
4
  import { Service, logColor } from "nodefony";
5
- //#region nodefony/service/authorization.ts
5
+ // nodefony/service/authorization.ts
6
6
  const serviceName = "authorization";
7
7
  /**
8
8
  * Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
@@ -90,5 +90,4 @@ function describeSubject(subject) {
90
90
  if (subject === null || subject === void 0) return String(subject);
91
91
  return subject.constructor?.name ?? typeof subject;
92
92
  }
93
- //#endregion
94
93
  export { Authorization, Authorization as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/service/cors.ts
1
+ // nodefony/service/cors.ts
2
2
  /**
3
3
  * Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
4
4
  * (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
@@ -77,5 +77,4 @@ var Cors = class {
77
77
  return h;
78
78
  }
79
79
  };
80
- //#endregion
81
80
  export { Cors, Cors as default };
@@ -1,5 +1,5 @@
1
1
  import { CsrfError } from "../errors/CsrfError.js";
2
- //#region nodefony/service/csrf.ts
2
+ // nodefony/service/csrf.ts
3
3
  /**
4
4
  * Méthodes HTTP « sûres » (RFC 9110 §9.2.1) — sans effet de bord par contrat,
5
5
  * donc hors du vecteur CSRF. Tout le reste (POST/PUT/PATCH/DELETE…) est traité
@@ -93,5 +93,4 @@ var Csrf = class {
93
93
  }
94
94
  }
95
95
  };
96
- //#endregion
97
96
  export { Csrf, Csrf as default };
@@ -1,6 +1,11 @@
1
+ import { compileProviderRoleMapping, isPlatformRole } from "../src/oauth/providerRoles.js";
1
2
  import { SecuredArea } from "../src/SecuredArea.js";
3
+ import { entryPointLocation } from "../src/entryPoint.js";
2
4
  import { LoginThrottler } from "../src/throttle/LoginThrottler.js";
5
+ import { InsecureTransportError } from "../errors/InsecureTransportError.js";
6
+ import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "../src/transport/CredentialTransportPolicy.js";
3
7
  import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
8
+ import { rolesWithin } from "../src/rolesWithin.js";
4
9
  import { mergeCspFragments } from "../src/csp.js";
5
10
  import { CsrfTokenManager } from "../src/csrfToken.js";
6
11
  import { CsrfError } from "../errors/CsrfError.js";
@@ -17,10 +22,11 @@ import { DEFAULT_SYSTEM_RULES, buildFrameAuthorizer, buildSystemRules } from "..
17
22
  import { recordAudit } from "../src/audit/recordAudit.js";
18
23
  import { readAuditContext } from "../src/audit/readAuditContext.js";
19
24
  import { SECURITY_AUDIT_CHANNEL, createAuditBridge } from "../src/audit/auditBridge.js";
20
- import { RequestContext, Service, logColor } from "nodefony";
21
- import { encoderFromConfig } from "@nodefony/user";
25
+ import { findZonesOpenToSignup } from "../src/openToSignup.js";
26
+ import { ROLES_WITHIN, RequestContext, Service, logColor } from "nodefony";
22
27
  import { randomBytes } from "node:crypto";
23
- //#region nodefony/service/firewall.ts
28
+ import { encoderFromConfig } from "@nodefony/user";
29
+ // nodefony/service/firewall.ts
24
30
  const serviceName = "firewall";
25
31
  /**
26
32
  * Réponse partagée de {@link Firewall.publishedProtectedResources} quand il n'y
@@ -87,6 +93,7 @@ var Firewall = class extends Service {
87
93
  #cspFragments = null;
88
94
  #config = null;
89
95
  #configError = null;
96
+ #longLivedRevalidation = null;
90
97
  constructor(module) {
91
98
  super(serviceName, module.container, module.notificationsCenter, module.options);
92
99
  this.module = module;
@@ -102,6 +109,7 @@ var Firewall = class extends Service {
102
109
  }
103
110
  this.#roleHierarchy = new RoleHierarchyWalker(this.#config.roleHierarchy);
104
111
  this.container?.set("roleHierarchy", this.#roleHierarchy);
112
+ this.container?.set(ROLES_WITHIN, rolesWithin(() => this.container?.get("users") ?? null, this.#roleHierarchy));
105
113
  this.#provisionSharedServices(this.#config);
106
114
  if (this.#config.csrf.enabled) {
107
115
  this.#csrf = new Csrf(this.#config.csrf, [...this.#config.csrf.trustedOrigins, ...this.#config.cors.origins]);
@@ -122,6 +130,7 @@ var Firewall = class extends Service {
122
130
  this.#areas = list;
123
131
  this.#instantiateAuthenticators(list);
124
132
  }
133
+ for (const z of findZonesOpenToSignup(this.#config)) this.log(`${z.message} → ${z.action}`, "WARNING");
125
134
  if (!this.#configError) {
126
135
  this.#wireRealtime();
127
136
  this.log(`Firewall ready — ${this.#areas?.length ?? 0} area(s), ${this.#authenticators?.size ?? 0} authenticator(s)`, "DEBUG");
@@ -137,7 +146,7 @@ var Firewall = class extends Service {
137
146
  pattern: area.pattern,
138
147
  host: area.host
139
148
  } : { pattern: area.pattern };
140
- realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null));
149
+ realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null, () => this.container?.get("users") ?? null));
141
150
  wired = true;
142
151
  }
143
152
  const configRules = (this.#config?.realtimeChannels ?? []).map((r) => ({
@@ -172,6 +181,10 @@ var Firewall = class extends Service {
172
181
  this.log(wired ? "Realtime data plane locked — WS handshake authenticators (RBAC) + frame authorizer + audit channel wired" : "Realtime data plane locked — frame authorizer (system floor) wired without qualifying zone; system channels closed to anonymous", "DEBUG");
173
182
  }
174
183
  #provisionSharedServices(config) {
184
+ this.container?.set("credentialTransport", new CredentialTransportPolicy({
185
+ enforce: shouldEnforceCredentialTransport(this.kernel?.environment, config.allowInsecureCredentials),
186
+ log: (message) => this.log(message, "WARNING")
187
+ }));
175
188
  const specs = Object.values(config.encoders);
176
189
  if (specs.length > 0) this.container?.set("passwordEncoder", encoderFromConfig(specs));
177
190
  const rl = config.rateLimit;
@@ -263,17 +276,27 @@ var Firewall = class extends Service {
263
276
  describe() {
264
277
  const config = this.#config;
265
278
  const mounted = this.#authenticators;
266
- const zones = (this.#areas ?? []).map((a) => ({
267
- name: a.name,
268
- pattern: a.pattern.source,
269
- security: a.security,
270
- stateless: a.stateless,
271
- mode: a.mode,
272
- authenticators: [...a.authenticators],
273
- allowsAnonymous: a.authenticators.includes("anonymous"),
274
- host: a.host ?? null,
275
- realtime: a.realtime
276
- }));
279
+ const open = new Map((config ? findZonesOpenToSignup(config) : []).map((z) => [z.zone, z]));
280
+ const zones = (this.#areas ?? []).map((a) => {
281
+ const notice = open.get(a.name);
282
+ return {
283
+ name: a.name,
284
+ pattern: a.pattern.source,
285
+ security: a.security,
286
+ stateless: a.stateless,
287
+ mode: a.mode,
288
+ authenticators: [...a.authenticators],
289
+ allowsAnonymous: a.authenticators.includes("anonymous"),
290
+ host: a.host ?? null,
291
+ realtime: a.realtime,
292
+ roles: a.roles ? [...a.roles] : [],
293
+ openToSignup: notice !== void 0,
294
+ openToSignupNotice: notice ? {
295
+ message: notice.message,
296
+ action: notice.action
297
+ } : null
298
+ };
299
+ });
277
300
  const available = new Set(listAuthenticatorFactories());
278
301
  const names = new Set(available);
279
302
  if (mounted) for (const n of mounted.keys()) names.add(n);
@@ -340,12 +363,32 @@ var Firewall = class extends Service {
340
363
  const hierarchy = {};
341
364
  for (const [role, inherited] of Object.entries(raw)) hierarchy[role] = [...inherited];
342
365
  const walker = this.roleHierarchy;
366
+ const roles = Object.keys(hierarchy).map((role) => ({
367
+ role,
368
+ inherits: [...walker.reachableRoles([role])].filter((r) => r !== role).sort()
369
+ }));
370
+ const providerMappings = [];
371
+ for (const [provider, p] of Object.entries(this.#config?.oauth2.providers ?? {})) {
372
+ const mapping = compileProviderRoleMapping(p);
373
+ if (mapping === null) continue;
374
+ providerMappings.push({
375
+ provider,
376
+ sources: [...mapping.sources],
377
+ mappings: Object.keys(mapping.table).sort().map((from) => {
378
+ const to = mapping.table[from] ?? "";
379
+ return {
380
+ from,
381
+ to,
382
+ platform: isPlatformRole(to)
383
+ };
384
+ }),
385
+ allowPlatformRoles: p.allowPlatformRoles
386
+ });
387
+ }
343
388
  return {
344
389
  hierarchy,
345
- roles: Object.keys(hierarchy).map((role) => ({
346
- role,
347
- inherits: [...walker.reachableRoles([role])].filter((r) => r !== role).sort()
348
- }))
390
+ roles,
391
+ providerMappings
349
392
  };
350
393
  }
351
394
  /**
@@ -424,7 +467,7 @@ var Firewall = class extends Service {
424
467
  }
425
468
  /**
426
469
  * Pipeline complet de la zone : chaîne d'authenticators (selon `mode`) → ALS
427
- * → Zero Trust. Rejette (401, challenge RFC 7235 posé) ou résout.
470
+ * → Zero Trust. Rejette (401, challenge RFC 9110 §11 posé) ou résout.
428
471
  */
429
472
  async handleSecurity(context) {
430
473
  if (this.#configError) throw new AuthenticationError("Security configuration invalid");
@@ -449,17 +492,22 @@ var Firewall = class extends Service {
449
492
  this.#recordAuth(context, area, "auth.unverifiable", "failure", "verifier_unavailable", null);
450
493
  throw error;
451
494
  }
495
+ if (error instanceof InsecureTransportError) {
496
+ this.#recordAuth(context, area, "auth.failure", "failure", "insecure_transport", null);
497
+ throw error;
498
+ }
452
499
  this.#setChallenge(context, area);
453
500
  this.#recordAuth(context, area, "auth.failure", "failure", "invalid_credentials", null);
454
501
  throw error;
455
502
  }
456
503
  if (token === null) {
457
- this.#setChallenge(context, area);
504
+ if (!this.#redirectToEntryPoint(context, area)) this.#setChallenge(context, area);
458
505
  this.#recordAuth(context, area, "auth.denied", "denied", "no_credentials", null);
459
506
  throw new AuthenticationError(`Authentication required for area "${area.name}"`);
460
507
  }
461
508
  RequestContext.set("user", token.getUser());
462
509
  RequestContext.set("token", token);
510
+ if (token.isAuthenticated()) context.userIdentifier = token.getUserIdentifier();
463
511
  if (!token.isAuthenticated() && token.type !== "anonymous") {
464
512
  this.#setChallenge(context, area);
465
513
  this.#recordAuth(context, area, "auth.denied", "denied", "unauthenticated", token.getUserIdentifier());
@@ -520,8 +568,13 @@ var Firewall = class extends Service {
520
568
  * synchronizer token (`context.csrfToken`) — HttpContext pose ensuite le cookie
521
569
  * lisible `csrf-token`. Sinon, hot-path GET = retour immédiat (aucun en-tête lu).
522
570
  * - **Étape 1 (globale)** : sur une mutation, défense Fetch Metadata / Origin
523
- * (rejet cross-site même sur route publique). Skippée si `@CsrfExempt` (webhook,
524
- * auth par signature/clé) ou `bypassFirewall` (callbacks OAuth).
571
+ * (rejet cross-site même sur route publique). Skippée SEULEMENT par `@CsrfExempt`
572
+ * (webhook, auth par signature/clé). `bypassFirewall` ne l'en dispense PAS : il
573
+ * retire l'authentification, pas la provenance — les routes de connexion sont
574
+ * en bypass, et c'est précisément là qu'un formulaire tiers ouvrirait chez la
575
+ * victime une session sur le compte de l'attaquant (login CSRF, CWE-352). Un
576
+ * appel de serveur à serveur (canal arrière OIDC) ne porte ni `Sec-Fetch-Site`
577
+ * ni `Origin` : il passe toujours.
525
578
  * - **Étape 2 (opt-in)** : sur une mutation `@CsrfProtect`, exige EN PLUS le
526
579
  * synchronizer token (en-tête `x-csrf-token` ≡ cookie + HMAC valide).
527
580
  *
@@ -529,7 +582,6 @@ var Firewall = class extends Service {
529
582
  */
530
583
  enforceCsrf(context) {
531
584
  if (!this.#csrf) return;
532
- if (context.resolver?.bypassFirewall) return;
533
585
  const ctx = context;
534
586
  if (!Csrf.isStateChanging(context.method)) {
535
587
  if (ctx.csrfProtect && this.#csrfTokens && ctx.csrfToken == null) ctx.csrfToken = this.#csrfTokens.issue();
@@ -577,6 +629,18 @@ var Firewall = class extends Service {
577
629
  return isPreflight ? 204 : void 0;
578
630
  }
579
631
  /**
632
+ * Le revalidateur de l'identité de la requête courante, pour une connexion
633
+ * longue qui la gardera (flux SSE). Même règle, même store de jetons que les
634
+ * sockets WebSocket — un transport qui en aurait une autre serait celui
635
+ * qu'un attaquant prendrait pour survivre à sa révocation.
636
+ *
637
+ * @returns `null` pour un anonyme ; sinon la fonction qui re-valide.
638
+ */
639
+ currentRevalidator() {
640
+ this.#longLivedRevalidation ??= new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null);
641
+ return this.#longLivedRevalidation.currentRevalidator();
642
+ }
643
+ /**
580
644
  * En-têtes de sécurité APPLICATIFS (P6 J5) — CSP, Referrer-Policy, isolation
581
645
  * cross-origin (COOP/COEP/CORP), Origin-Agent-Cluster, Permissions-Policy.
582
646
  * Posés sur toute réponse du pipeline (branché dans `handleHttp`). Complète le
@@ -690,6 +754,34 @@ var Firewall = class extends Service {
690
754
  }
691
755
  return token;
692
756
  }
757
+ /**
758
+ * Pose la redirection vers la page de connexion de la zone, quand la requête
759
+ * refusée est la navigation d'un navigateur. Cold path (refus seulement).
760
+ *
761
+ * La réponse est seulement MARQUÉE (`302` + `Location`) : le refus continue
762
+ * de remonter, et http rend une réponse déjà redirigée telle quelle au lieu
763
+ * d'un 401. Capability check : un contexte sans `redirect` (WebSocket) garde
764
+ * son refus.
765
+ *
766
+ * @returns `true` si la redirection est posée
767
+ */
768
+ #redirectToEntryPoint(context, area) {
769
+ if (area.entryPoint === void 0 || !("redirect" in context)) return false;
770
+ const request = context.request;
771
+ const header = (name) => {
772
+ const value = request.headers[name];
773
+ return typeof value === "string" ? value : void 0;
774
+ };
775
+ const location = entryPointLocation(area.entryPoint, {
776
+ method: request.method,
777
+ accept: header("accept"),
778
+ fetchMode: header("sec-fetch-mode"),
779
+ path: request.url.pathname + request.url.search
780
+ });
781
+ if (location === null) return false;
782
+ context.redirect(location, 302);
783
+ return true;
784
+ }
693
785
  #setChallenge(context, area) {
694
786
  const response = context.response;
695
787
  if (typeof response?.setHeader !== "function") return;
@@ -707,5 +799,4 @@ var Firewall = class extends Service {
707
799
  return super.log(pci, severity, msgid, msg);
708
800
  }
709
801
  };
710
- //#endregion
711
802
  export { Firewall, Firewall as default };