@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,9 +1,8 @@
1
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
1
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  }
8
- //#endregion
9
8
  export { __decorate as default };
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
1
2
  import config from "./nodefony/config/config.js";
2
3
  import { SecuredArea } from "./nodefony/src/SecuredArea.js";
3
4
  import { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
5
+ import { InsecureTransportError } from "./nodefony/errors/InsecureTransportError.js";
6
+ import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "./nodefony/src/transport/CredentialTransportPolicy.js";
4
7
  import { RoleHierarchyWalker } from "./nodefony/src/RoleHierarchyWalker.js";
5
8
  import { CsrfTokenManager } from "./nodefony/src/csrfToken.js";
6
9
  import { CsrfError } from "./nodefony/errors/CsrfError.js";
@@ -26,13 +29,13 @@ import { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthentica
26
29
  import { recordAudit } from "./nodefony/src/audit/recordAudit.js";
27
30
  import { readAuditContext } from "./nodefony/src/audit/readAuditContext.js";
28
31
  import { Firewall } from "./nodefony/service/firewall.js";
32
+ import { amrForFactor } from "./nodefony/src/sessionAuthentication.js";
29
33
  import { AuthFlow } from "./nodefony/service/authFlow.js";
30
34
  import { InvalidTargetError } from "./nodefony/errors/InvalidTargetError.js";
31
35
  import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./nodefony/src/token/tokenSort.js";
32
36
  import { matchesTokenStatus, tokenStatusOf } from "./nodefony/src/token/tokenStatus.js";
33
37
  import { MemoryTokenStore } from "./nodefony/src/token/MemoryTokenStore.js";
34
38
  import { getTokenStoreFactory, listTokenStores, registerTokenStore } from "./nodefony/src/token/tokenStoreRegistry.js";
35
- import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
36
39
  import { TokenService } from "./nodefony/service/tokenService.js";
37
40
  import { RemoteJwtVerifier } from "./nodefony/src/token/RemoteJwtVerifier.js";
38
41
  import { AccessTokenVerifierService } from "./nodefony/service/accessTokenVerifier.js";
@@ -44,11 +47,13 @@ import { Authorization } from "./nodefony/service/authorization.js";
44
47
  import { MemoryWebAuthnCredentialStore } from "./nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js";
45
48
  import { getWebAuthnStoreFactory, listWebAuthnStores, registerWebAuthnStore } from "./nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js";
46
49
  import { WebAuthnService } from "./nodefony/service/webAuthn.js";
50
+ import { KeycloakAdminClient, KeycloakAdminError, keycloakAdminTarget, keycloakLoginThemeCheck, readKeycloakLoginTheme } from "./nodefony/keycloak/keycloakAdmin.js";
47
51
  import { OAuth2Client, OAuth2RequestError, OAuth2Tokens, createCodeChallenge, generateCodeVerifier, generateState } from "./nodefony/src/oauth/oauth2Client.js";
48
52
  import { discoverAuthorizationServer } from "./nodefony/src/oauth/metadata.js";
49
- import { createDiscoveredOidcProvider, createOidcProvider } from "./nodefony/src/oauth/providers/oidc.js";
53
+ import { createDiscoveredOidcProvider, createOidcProvider, readLogoutTokenClaims } from "./nodefony/src/oauth/providers/oidc.js";
50
54
  import { createGithubProvider } from "./nodefony/src/oauth/providers/github.js";
51
- import { getOAuthProviderFactory, listOAuthProviders, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
55
+ import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
56
+ import { diagnoseOAuthProvider } from "./nodefony/src/oauth/providerDiagnosis.js";
52
57
  import { OAuth2Service } from "./nodefony/service/oauth2.js";
53
58
  import { TOKEN_FACETS } from "./nodefony/src/token/tokenFilters.js";
54
59
  import { ApiKeyError } from "./nodefony/errors/ApiKeyError.js";
@@ -69,24 +74,28 @@ import { getWebhookStoreFactory, listWebhookStores, registerWebhookStore } from
69
74
  import { SsrfError } from "./nodefony/errors/SsrfError.js";
70
75
  import { assertPublicUrl, isBlockedAddress } from "./nodefony/src/net/ssrfGuard.js";
71
76
  import { WebhookService } from "./nodefony/service/webhooks.js";
77
+ import { KeycloakService } from "./nodefony/service/keycloak.js";
72
78
  import SecuritySecrets from "./nodefony/command/security-secrets.js";
73
79
  import SecurityUserAdd from "./nodefony/command/security-user-add.js";
74
80
  import SecurityUserList from "./nodefony/command/security-user-list.js";
75
81
  import SecurityUserDelete from "./nodefony/command/security-user-delete.js";
76
82
  import SecurityUserPassword from "./nodefony/command/security-user-password.js";
77
- import SecurityToken from "./nodefony/command/security-token.js";
83
+ import { publicRoutesNotice } from "./nodefony/src/boot/publicRoutesNotice.js";
84
+ import SecurityMcpToken from "./nodefony/command/security-mcp-token.js";
85
+ import SecurityKeycloakRealm from "./nodefony/keycloak/security-keycloak-realm.js";
86
+ import SecurityOAuthDoctor from "./nodefony/command/security-oauth-doctor.js";
78
87
  import { createSecurityAdminApi, parseAuditQuery, registerSecurityAdminApi } from "./nodefony/src/admin/SecurityAdminApi.js";
79
- import { registerUserRevocationCascade } from "./nodefony/src/admin/userRevocationCascade.js";
80
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
81
- import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
88
+ import { registerUserRevocationCascade, usersEventBusNotice } from "./nodefony/src/admin/userRevocationCascade.js";
89
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
82
90
  import { tokenStatusCriteria } from "./nodefony/src/token/tokenCriteria.js";
91
+ import { requestClientCredentialsToken } from "./nodefony/src/oauth/clientCredentials.js";
83
92
  import { AccessDeniedError } from "./nodefony/errors/AccessDeniedError.js";
84
93
  import "./nodefony/errors/index.js";
85
94
  import "./nodefony/contracts/index.js";
86
- import { Kernel, Module, services } from "nodefony";
95
+ import { Module, services } from "nodefony";
87
96
  import { fileURLToPath } from "node:url";
88
97
  import { registerUserAdminApi } from "@nodefony/user";
89
- //#region index.ts
98
+ // index.ts
90
99
  /**
91
100
  * `@nodefony/security` — couche de sécurité de Nodefony (refonte 2026, P6).
92
101
  *
@@ -96,7 +105,7 @@ import { registerUserAdminApi } from "@nodefony/user";
96
105
  * + Zod validée au boot (fail-closed si invalide). Consomme `@nodefony/user`
97
106
  * (IUser/IUserProvider/IPasswordVerifier) — jamais l'inverse.
98
107
  *
99
- * Livré : firewall (zones, mode first|all, challenge RFC 7235), authenticators
108
+ * Livré : firewall (zones, mode first|all, challenge RFC 9110 §11), authenticators
100
109
  * `anonymous`/`userpassword` (Basic RFC 7617)/`session` (BFF, J3), flux
101
110
  * login/logout/me (`AuthFlow`, anti-fixation + throttling partagé).
102
111
  * Jwt/oauth2/mtls/apikey, CORS, CSRF, autorisation par décorateurs et data
@@ -110,7 +119,9 @@ let Security = class Security extends Module {
110
119
  this.addCommand(SecurityUserList);
111
120
  this.addCommand(SecurityUserDelete);
112
121
  this.addCommand(SecurityUserPassword);
113
- this.addCommand(SecurityToken);
122
+ this.addCommand(SecurityMcpToken);
123
+ this.addCommand(SecurityKeycloakRealm);
124
+ this.addCommand(SecurityOAuthDoctor);
114
125
  }
115
126
  /**
116
127
  * JSON Schema de la config security → data plane admin (config riche Studio).
@@ -135,7 +146,28 @@ let Security = class Security extends Module {
135
146
  registerSecurityAdminApi(registry, container);
136
147
  registerUserAdminApi(registry, container);
137
148
  }
138
- if (this.kernel && container) registerUserRevocationCascade(this.kernel, container);
149
+ const kernel = this.kernel;
150
+ if (kernel && container) {
151
+ registerUserRevocationCascade(kernel, container);
152
+ kernel.once("onPostReady", () => {
153
+ const notice = usersEventBusNotice(container, kernel);
154
+ if (notice) kernel.reportBootNotice(notice);
155
+ });
156
+ }
157
+ return this;
158
+ }
159
+ /**
160
+ * Déclare au bilan de démarrage les routes métier laissées PUBLIQUES.
161
+ *
162
+ * `onReady` et pas `onBoot` : les zones sont compilées par le pare-feu à son
163
+ * propre `onBoot`, posé APRÈS celui du module — à `onBoot` la liste est encore
164
+ * vide. `onReady` précède l'écriture du bilan (`var/last-boot.json`).
165
+ */
166
+ async onKernelReady() {
167
+ const firewall = this.kernel?.container?.get("firewall");
168
+ if (!firewall) return this;
169
+ const notice = publicRoutesNotice(firewall.describe().zones);
170
+ if (notice) this.kernel?.reportBootNotice(notice);
139
171
  return this;
140
172
  }
141
173
  };
@@ -150,8 +182,8 @@ Security = __decorate([services([
150
182
  OAuth2Service,
151
183
  AuditService,
152
184
  TotpService,
153
- WebhookService
154
- ]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Security);
185
+ WebhookService,
186
+ KeycloakService
187
+ ])], Security);
155
188
  var security_default = Security;
156
- //#endregion
157
- export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InvalidTargetError, JwtAuthenticator, JwtKeystore, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Client, OAuth2RequestError, OAuth2Service, OAuth2Tokens, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createCodeChallenge, createDiscoveredOidcProvider, createGithubProvider, createOidcProvider, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, disableTotp, discoverAuthorizationServer, encryptSecret, generateApiKey, generateCodeVerifier, generateEphemeralKey, generateState, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, resolveJwtRuntime, securityConfigJsonSchema, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
189
+ export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, CredentialTransportPolicy, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InsecureTransportError, InvalidTargetError, JwtAuthenticator, JwtKeystore, KeycloakAdminClient, KeycloakAdminError, KeycloakService, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Client, OAuth2RequestError, OAuth2Service, OAuth2Tokens, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, amrForFactor, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createCodeChallenge, createDiscoveredOidcProvider, createGithubProvider, createOidcProvider, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, diagnoseOAuthProvider, disableTotp, discoverAuthorizationServer, encryptSecret, generateApiKey, generateCodeVerifier, generateEphemeralKey, generateState, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, keycloakAdminTarget, keycloakLoginThemeCheck, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, oauthProviderRequiresIssuer, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, readKeycloakLoginTheme, readLogoutTokenClaims, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, requestClientCredentialsToken, resolveJwtRuntime, securityConfigJsonSchema, shouldEnforceCredentialTransport, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
@@ -3,7 +3,7 @@ import { ADMIN_SCOPE_READ, ADMIN_SCOPE_WRITE, AGENT_TARGETS, CONSOLE_DATA_RUN_PR
3
3
  import path from "node:path";
4
4
  import { existsSync } from "node:fs";
5
5
  import { spawnSync } from "node:child_process";
6
- //#region nodefony/command/security-token.ts
6
+ // nodefony/command/security-mcp-token.ts
7
7
  const options = {
8
8
  runProfile: CONSOLE_DATA_RUN_PROFILE,
9
9
  helpGroup: "COMPTES ET SECRETS",
@@ -38,13 +38,43 @@ function ttlSeconds(raw) {
38
38
  if (minutes > TTL_MAX_MINUTES) return /* @__PURE__ */ new Error(`--ttl est borné à ${TTL_MAX_MINUTES} minutes (30 jours) — un jeton posé dans un fichier est une clé, et une clé se remplace`);
39
39
  return minutes * 60;
40
40
  }
41
+ /**
42
+ * Durée d'un jeton POSÉ dans un agent : 8 h, une journée de travail.
43
+ *
44
+ * L'en-tête d'un agent est figé, rien ne le rafraîchit : le défaut de la
45
+ * configuration (15 min, taillé pour un client d'API qui renouvelle son jeton)
46
+ * y produisait un 401 au bout d'un quart d'heure, qui n'accusait pas
47
+ * l'expiration. Huit heures évitent la relance dans la journée et bornent une
48
+ * fuite à quelques heures. Le défaut global (`jwt.accessTtlS`) ne bouge pas :
49
+ * le relever allongerait TOUS les jetons de l'application.
50
+ */
51
+ const AGENT_TTL_MINUTES = 480;
52
+ /**
53
+ * Durée demandée à l'émetteur : `--ttl` explicite, sinon 8 h quand le jeton
54
+ * part chez les agents, sinon rien — la configuration décide.
55
+ *
56
+ * @param raw - la valeur de `--ttl` telle que tapée, ou rien
57
+ * @param write - le jeton sera posé dans la configuration des agents
58
+ * @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
59
+ */
60
+ function tokenTtlSeconds(raw, write) {
61
+ if (raw !== void 0) return ttlSeconds(raw);
62
+ return write ? AGENT_TTL_MINUTES * 60 : void 0;
63
+ }
64
+ /** « 8 h », « 1 h 30 », « 15 min » — la durée telle qu'on la dit. */
65
+ function humanDuration(seconds) {
66
+ const minutes = Math.round(seconds / 60);
67
+ if (minutes < 60) return `${minutes} min`;
68
+ const rest = minutes % 60;
69
+ return rest === 0 ? `${minutes / 60} h` : `${Math.floor(minutes / 60)} h ${rest}`;
70
+ }
41
71
  const GREEN = "\x1B[32m";
42
72
  const YELLOW = "\x1B[33m";
43
73
  const DIM = "\x1B[2m";
44
74
  const BOLD = "\x1B[1m";
45
75
  const RESET = "\x1B[0m";
46
76
  /**
47
- * `nodefony security:token` — émet un jeton d'accès pour une porte de cette
77
+ * `nodefony security:mcp:token` — émet un jeton d'accès pour une porte de cette
48
78
  * application (la porte MCP par défaut).
49
79
  *
50
80
  * **Pourquoi une commande, et pas un `curl`.** Le jeton s'obtenait par un appel
@@ -58,18 +88,26 @@ const RESET = "\x1B[0m";
58
88
  * raison d'être de la liaison d'audience. La commande la vise d'elle-même,
59
89
  * `--resource` ne sert qu'à en viser une autre.
60
90
  *
61
- * Suit `security:secrets` : `--write` pose la valeur là où l'AGENT la lit
62
- * (gitignoré), jamais dans le `.env` commité, et ne remplace jamais une valeur
63
- * existante — une rotation est un geste explicite.
91
+ * `--write` pose la valeur là où l'AGENT la lit (gitignoré), jamais dans le
92
+ * `.env` de l'application. Relancée, elle REMPLACE le jeton chez les agents qui
93
+ * le portent déjà — la rotation est le geste le plus fréquent — et n'en câble
94
+ * aucun nouveau sans le proposer.
64
95
  */
65
- var SecurityToken = class extends Command {
96
+ var SecurityMcpToken = class extends Command {
66
97
  constructor(cli) {
67
- super("security:token", `émet un jeton d'accès pour la porte MCP`, cli, options);
98
+ super("security:mcp:token", `émet un jeton d'accès pour la porte MCP`, cli, {
99
+ ...options,
100
+ examples: [
101
+ "nodefony security:mcp:token",
102
+ "nodefony security:mcp:token --write",
103
+ "nodefony security:mcp:token alice --scope admin:read --ttl 60"
104
+ ]
105
+ });
68
106
  this.addArgument("[identifier]", "compte porteur du jeton (défaut : admin)");
69
107
  this.addOption("-s, --scope <scopes>", `scopes demandés, séparés par des espaces (défaut : « ${ADMIN_SCOPE_READ} » ; ajouter « ${ADMIN_SCOPE_WRITE} » pour les mutations)`);
70
108
  this.addOption("-r, --resource <uri>", "audience visée (défaut : la porte MCP de cette application)");
71
109
  this.addOption("-a, --agent <noms>", "agents à servir, séparés par des virgules (défaut : ceux détectés ; « none » pour aucun)");
72
- this.addOption("-t, --ttl <duree>", "durée de validité, en minutes (défaut : celle de la config, 15 min)");
110
+ this.addOption("-t, --ttl <minutes>", "durée de validité, en minutes (défaut : 8 h avec --write, sinon celle de la config, 15 min)");
73
111
  this.addOption("-w, --write", `pose ${MCP_TOKEN_ENV} dans la configuration des agents présents`);
74
112
  this.addOption("-j, --json", "sortie JSON (scripts/CI)");
75
113
  }
@@ -239,7 +277,15 @@ var SecurityToken = class extends Command {
239
277
  const resource = opts.resource ?? this.#defaultResource();
240
278
  const requestedScopes = (opts.scope ?? "").split(/\s+/u).filter(Boolean);
241
279
  const scopes = requestedScopes.length > 0 ? requestedScopes : [ADMIN_SCOPE_READ];
242
- const ttlS = ttlSeconds(opts.ttl);
280
+ let write = opts.write === true;
281
+ if (!write && !opts.json && process.stdin.isTTY) {
282
+ await this.loadPrompts();
283
+ write = await this.prompts.confirm({
284
+ message: `Poser ${MCP_TOKEN_ENV} dans la configuration des agents présents ?`,
285
+ default: true
286
+ });
287
+ }
288
+ const ttlS = tokenTtlSeconds(opts.ttl, write);
243
289
  if (ttlS instanceof Error) {
244
290
  this.log(ttlS.message, "ERROR");
245
291
  process.exitCode = 1;
@@ -262,7 +308,7 @@ var SecurityToken = class extends Command {
262
308
  porteur obtiendrait un jeton pour la ressource de son choix :
263
309
 
264
310
  use("@nodefony/security", {
265
- jwt: { audiences: ["${resource}"] },\n })\n\n → puis npm run build (le runtime lit le dist)\n` : ` La porte est servie par un module de DÉVELOPPEMENT, absent en\n « ${env} » : un jeton pour une porte absente n'aurait personne\n pour l'accepter.\n\n → NODE_ENV=development nodefony security:token${opts.write ? " --write" : ""}\n`) + ` → ou vise une autre porte : --resource <uri>`, "ERROR");
311
+ jwt: { audiences: ["${resource}"] },\n })\n\n → puis npm run build (le runtime lit le dist)\n` : ` La porte est servie par un module de DÉVELOPPEMENT, absent en\n « ${env} » : un jeton pour une porte absente n'aurait personne\n pour l'accepter.\n\n → NODE_ENV=development nodefony security:mcp:token${opts.write ? " --write" : ""}\n`) + ` → ou vise une autre porte : --resource <uri>`, "ERROR");
266
312
  process.exitCode = 1;
267
313
  return this;
268
314
  }
@@ -271,30 +317,23 @@ var SecurityToken = class extends Command {
271
317
  const token = issued.access_token;
272
318
  const accordes = issued.scope.split(/\s+/u).filter(Boolean);
273
319
  const nonAccordes = scopes.filter((s) => !accordes.includes(s));
320
+ const expiresAt = new Date(Date.now() + issued.expires_in * 1e3);
274
321
  if (opts.json) {
275
322
  process.stdout.write(`${JSON.stringify({
276
323
  access_token: token,
277
324
  resource,
278
325
  scopes: accordes,
279
326
  requested: scopes,
280
- expires_in: issued.expires_in
327
+ expires_in: issued.expires_in,
328
+ expires_at: expiresAt.toISOString()
281
329
  }, null, 2)}\n`);
282
330
  return this;
283
331
  }
284
332
  const w = (s) => {
285
333
  process.stdout.write(s);
286
334
  };
287
- let write = opts.write === true;
288
- if (!write && process.stdin.isTTY) {
289
- await this.loadPrompts();
290
- write = await this.prompts.confirm({
291
- message: `Poser ${MCP_TOKEN_ENV} dans la configuration des agents présents ?`,
292
- default: true
293
- });
294
- }
295
- if (this.#ephemeralKey()) w(`\n${YELLOW}⚠ Clé de signature ÉPHÉMÈRE — ce jeton sera REFUSÉ.${RESET}\n${DIM} Cette application n'a pas de clé persistante : chaque process en génère\n une au démarrage. Le jeton ci-dessous n'est vérifiable que par le process\n qui vient de le signer — pas par le serveur en marche, qui a la sienne.\n → déclare une source de clés dans nodefony.config.ts :\n use("@nodefony/security", { jwt: { keystore: { dir: "var/keys" } } })\n ou, en production, keySetJson depuis l'environnement.${RESET}\n`);
296
- const minutes = Math.round(issued.expires_in / 60);
297
- w(`\n${BOLD}🔑 Jeton d'accès${RESET} ${DIM}— compte ${identifier}, audience ${resource}${RESET}\n${DIM} valable ${minutes} min${accordes.length ? `, scopes : ${accordes.join(" ")}` : ", aucun scope"}${RESET}\n\n`);
335
+ if (this.#ephemeralKey()) w(`\n${YELLOW}⚠ Clé de signature ÉPHÉMÈRE — ce jeton sera REFUSÉ.${RESET}\n${DIM} Cette application n'a pas de clé persistante : chaque process en génère\n une au démarrage. Le jeton ci-dessous n'est vérifiable que par le process\n qui vient de le signer — pas par le serveur en marche, qui a la sienne.\n → déclare une source de clés dans nodefony.config.ts :\n use("@nodefony/security", { jwt: { keystore: { dir: "var/keys" } } })\n ou, en production, keySetJson depuis l'environnement (NF_JWT_KEYSET —\n npx nodefony security:secrets --jwt-keyset).${RESET}\n`);
336
+ w(`\n${BOLD}🔑 Jeton d'accès${RESET} ${DIM}— compte ${identifier}, audience ${resource}${RESET}\n${DIM} valable ${humanDuration(issued.expires_in)}, expire le ${expiresAt.toLocaleString()}${accordes.length ? `, scopes : ${accordes.join(" ")}` : ", aucun scope"}${RESET}\n\n`);
298
337
  if (nonAccordes.length > 0) w(`${DIM} ⚠️ non accordé(s) : ${nonAccordes.join(" ")} — réservé(s) au rôle d'administration, que le compte « ${identifier} » ne porte pas.${RESET}\n\n`);
299
338
  if (write) {
300
339
  const requested = requestedAgents(opts.agent);
@@ -332,5 +371,4 @@ var SecurityToken = class extends Command {
332
371
  return this;
333
372
  }
334
373
  };
335
- //#endregion
336
- export { SecurityToken as default, ttlSeconds };
374
+ export { SecurityMcpToken as default, tokenTtlSeconds, ttlSeconds };
@@ -0,0 +1,68 @@
1
+ import { Command } from "nodefony";
2
+ // nodefony/command/security-oauth-doctor.ts
3
+ const options = {
4
+ helpGroup: "COMPTES ET SECRETS",
5
+ showBanner: false,
6
+ kernelEvent: "onReady",
7
+ quietBoot: true
8
+ };
9
+ const GREEN = "\x1B[32m";
10
+ const RED = "\x1B[31m";
11
+ const YELLOW = "\x1B[33m";
12
+ const DIM = "\x1B[2m";
13
+ const RESET = "\x1B[0m";
14
+ /** Codes de sortie (sysexits). */
15
+ const EXIT_FAILED = 1;
16
+ const EXIT_CONFIG = 78;
17
+ const LABELS = {
18
+ discovery: "découverte",
19
+ authorization: "URL de retour",
20
+ token: "secret",
21
+ theme: "thème"
22
+ };
23
+ function mark(check) {
24
+ switch (check.status) {
25
+ case "ok": return `${GREEN}✓${RESET}`;
26
+ case "failed": return `${RED}✗${RESET}`;
27
+ case "inconclusive": return `${YELLOW}?${RESET}`;
28
+ case "skipped": return `${DIM}–${RESET}`;
29
+ }
30
+ }
31
+ /**
32
+ * `nodefony security:oauth:doctor` — dit POURQUOI un fournisseur OAuth refuse
33
+ * l'application, sans qu'un humain ait à se connecter.
34
+ *
35
+ * Trois verdicts distincts là où le journal ne montre qu'un 401 : l'émetteur
36
+ * se déclare autrement que la configuration ne l'écrit, l'URL de retour n'est
37
+ * pas enregistrée, le secret est faux. La même analyse alimente
38
+ * `nodefony doctor --live` (famille « Fournisseurs OAuth ») et l'endpoint
39
+ * d'administration `security/oauth/diagnosis` : une seule implémentation, celle
40
+ * du service `oauth2`.
41
+ *
42
+ * Valable pour tout fournisseur : les sondes passent par le fournisseur que le
43
+ * login construit, Keycloak n'y a rien de particulier.
44
+ */
45
+ var SecurityOAuthDoctor = class extends Command {
46
+ constructor(cli) {
47
+ super("security:oauth:doctor", "diagnostique le branchement des fournisseurs OAuth", cli, {
48
+ ...options,
49
+ examples: ["nodefony security:oauth:doctor", "nodefony security:oauth:doctor --provider github --json"]
50
+ });
51
+ this.addOption("-p, --provider <nom>", "un seul fournisseur de oauth2.providers");
52
+ this.addOption("-j, --json", "sortie JSON (scripts/CI)");
53
+ }
54
+ async generate(opts) {
55
+ const oauth = this.kernel?.container?.get("oauth2");
56
+ const diagnoses = oauth?.isEnabled() === true ? await oauth.diagnose(opts.provider) : [];
57
+ if (opts.json) process.stdout.write(`${JSON.stringify(diagnoses, null, 2)}\n`);
58
+ else if (diagnoses.length === 0) process.stderr.write("aucun fournisseur OAuth à diagnostiquer — `security.oauth2` est désactivé ou ne déclare aucun fournisseur\n");
59
+ else for (const diagnosis of diagnoses) {
60
+ process.stdout.write(`${diagnosis.ok ? GREEN : RED}${diagnosis.provider}${RESET}\n`);
61
+ for (const check of diagnosis.checks) process.stdout.write(` ${mark(check)} ${LABELS[check.name].padEnd(14)}${check.message}\n`);
62
+ }
63
+ if (diagnoses.length === 0) process.exitCode = EXIT_CONFIG;
64
+ else if (diagnoses.some((d) => !d.ok)) process.exitCode = EXIT_FAILED;
65
+ return this;
66
+ }
67
+ };
68
+ export { SecurityOAuthDoctor as default };
@@ -1,10 +1,11 @@
1
1
  import { readIfPresentSync } from "../src/token/secretFile.js";
2
- import { Command, diskManifestReader, manifestFileWith, readManifestCode } from "nodefony";
3
- import { randomBytes } from "node:crypto";
2
+ import { generateKeySet } from "../src/token/JwtKeystore.js";
3
+ import { Command, ENV_FILE, diskManifestReader, manifestFileWith, readManifestCode } from "nodefony";
4
4
  import path from "node:path";
5
5
  import { appendFileSync } from "node:fs";
6
+ import { randomBytes } from "node:crypto";
6
7
  import { spawnSync } from "node:child_process";
7
- //#region nodefony/command/security-secrets.ts
8
+ // nodefony/command/security-secrets.ts
8
9
  const options = {
9
10
  helpGroup: "COMPTES ET SECRETS",
10
11
  showBanner: false,
@@ -45,11 +46,11 @@ const ROLES = {
45
46
  },
46
47
  NF_CSRF_SECRET: {
47
48
  protected: "signe les jetons anti-rejeu des mutations (`@CsrfProtect`)",
48
- without: "en cluster, le jeton émis par un pod est rejeté par les autres"
49
+ without: "démarrage REFUSÉ en production (code 78) — une valeur tirée au hasard différerait d'un exemplaire à l'autre"
49
50
  },
50
- "jwt.keystore": {
51
- protected: "signe les JWT (clé Ed25519, rotation gérée par le keystore)",
52
- without: "chaque process signe avec la sienne : un jeton émis par la CLI est refusé par le serveur"
51
+ NF_JWT_KEYSET: {
52
+ protected: "signe les jetons JWT (paire Ed25519) — la MÊME pour tous les pods et workers",
53
+ without: "démarrage REFUSÉ en production dès que l'application émet des jetons — sinon 401 au hasard derrière plusieurs exemplaires, et tous les jetons perdus au redémarrage"
53
54
  }
54
55
  };
55
56
  /**
@@ -68,25 +69,34 @@ const ROLES = {
68
69
  */
69
70
  var SecuritySecrets = class extends Command {
70
71
  constructor(cli) {
71
- super("security:secrets", "engendre les clés de chiffrement du module security", cli, options);
72
+ super("security:secrets", "engendre les clés de chiffrement du module security", cli, {
73
+ ...options,
74
+ examples: [
75
+ "nodefony security:secrets",
76
+ "nodefony security:secrets --write",
77
+ "nodefony security:secrets --jwt-keyset"
78
+ ]
79
+ });
72
80
  this.addOption("-j, --json", "sortie JSON (scripts/CI)");
73
81
  this.addOption("-w, --write", "écrit les clés manquantes dans le .env du projet (jamais de remplacement)");
82
+ this.addOption("-e, --env", "les 4 secrets en lignes CLÉ=valeur (docker run --env-file, gestionnaire de secrets)");
83
+ this.addOption("-k, --jwt-keyset", "génère la valeur de NF_JWT_KEYSET (une ligne JSON, à ranger dans le gestionnaire de secrets)");
74
84
  }
75
85
  /** Racine du projet (kernel booté) — repli cwd. */
76
86
  #root() {
77
87
  return this.kernel?.path ?? process.cwd();
78
88
  }
79
89
  /**
80
- * `true` si `.env.local` est SUIVI par git — y écrire des secrets les mènerait
81
- * au commit (convention B : `*.local` doit être gitignoré). Best-effort : git
82
- * absent / hors repo → `false` (on écrit).
90
+ * `true` si `.env` est SUIVI par git — y écrire des secrets les mènerait au
91
+ * commit (`.env` doit être gitignoré). Best-effort : git absent / hors repo →
92
+ * `false` (on écrit).
83
93
  */
84
94
  #dotenvTracked() {
85
95
  try {
86
96
  return spawnSync("git", [
87
97
  "ls-files",
88
98
  "--error-unmatch",
89
- ".env.local"
99
+ ENV_FILE
90
100
  ], {
91
101
  cwd: this.#root(),
92
102
  stdio: "ignore"
@@ -104,15 +114,24 @@ var SecuritySecrets = class extends Command {
104
114
  }
105
115
  }
106
116
  async generate(opts) {
117
+ if (opts.jwtKeyset) {
118
+ process.stdout.write(await generateKeySet() + "\n");
119
+ return this;
120
+ }
107
121
  const gen = () => randomBytes(32).toString("base64");
108
122
  const secrets = {};
109
123
  for (const k of KEYS) secrets[k] = gen();
110
124
  if (opts.json) {
125
+ secrets["NF_JWT_KEYSET"] = await generateKeySet();
111
126
  process.stdout.write(JSON.stringify(secrets, null, 2) + "\n");
112
127
  return this;
113
128
  }
114
- const dotenvLocal = this.#read(".env.local");
115
- const dotenv = this.#read(".env") + "\n" + dotenvLocal;
129
+ if (opts.env) {
130
+ secrets["NF_JWT_KEYSET"] = await generateKeySet();
131
+ process.stdout.write(Object.entries(secrets).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
132
+ return this;
133
+ }
134
+ const dotenv = this.#read(ENV_FILE);
116
135
  const envTs = this.#read("env.ts");
117
136
  const cfgTs = readManifestCode(this.#root(), diskManifestReader);
118
137
  const cfgFile = manifestFileWith(this.#root(), diskManifestReader, /satisfies\s+ISecurityConfigInput/);
@@ -130,18 +149,18 @@ var SecuritySecrets = class extends Command {
130
149
  process.stdout.write(s);
131
150
  };
132
151
  w(`\n${BOLD}🔐 Secrets du module security${RESET} ${DIM}— 4 secrets, 3 fichiers${RESET}\n\n`);
133
- for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "jwt.keystore" ? /keystore\s*:/u.test(cfgTs) : new RegExp(`^\\s*${name}\\s*=`, "m").test(dotenv)) ? "\x1B[32m✓" : "\x1B[33m○"}${RESET} ${BOLD}${name.padEnd(16)}${RESET}${DIM}${role.protected}${RESET}\n ${DIM}sans → ${role.without}${RESET}\n`);
134
- const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(dotenvLocal);
135
- w(`\n${DIM} · NF_MCP_TOKEN n'est PAS un secret de cette application, et n'a rien à\n faire dans cette liste : c'est un jeton qu'elle ÉMET, présenté par un\n agent pour entrer. Aucun code d'ici ne le lit. Il se pose chez l'agent\n qui le porte — nodefony security:token --write.${RESET}\n`);
136
- if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env.local — rien ne la lit,\n tu peux la retirer.${RESET}\n`);
152
+ for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "NF_JWT_KEYSET" ? cfgTs.includes(name) : new RegExp(`^\\s*${name}\\s*=`, "m").test(dotenv)) ? "\x1B[32m✓" : "\x1B[33m○"}${RESET} ${BOLD}${name.padEnd(16)}${RESET}${DIM}${role.protected}${RESET}\n ${DIM}sans → ${role.without}${RESET}\n`);
153
+ const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(dotenv);
154
+ w(`\n${DIM} · NF_MCP_TOKEN n'est PAS un secret de cette application, et n'a rien à\n faire dans cette liste : c'est un jeton qu'elle ÉMET, présenté par un\n agent pour entrer. Aucun code d'ici ne le lit. Il se pose chez l'agent\n qui le porte — nodefony security:mcp:token --write.${RESET}\n`);
155
+ if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env — rien ne la lit,\n tu peux la retirer.${RESET}\n`);
137
156
  w(`\n${YELLOW}⚠ rien ne se tape dans le terminal : chaque bloc se colle dans le fichier indiqué.${RESET}\n\n`);
138
- w(`${BOLD}1. Fichier ${CYAN}.env.local${RESET}${BOLD} — les valeurs${RESET} ${DIM}(gitignoré ; .env commité = défauts NON-secrets)${RESET}\n`);
157
+ w(`${BOLD}1. Fichier ${CYAN}.env${RESET}${BOLD} — les valeurs${RESET} ${DIM}(valeurs du poste, gitignoré ; la notice est .env.example)${RESET}\n`);
139
158
  if (missingInDotenv.length === 0) w(` ${GREEN}✓ les 3 clés y sont déjà${RESET} ${DIM}(rien à faire — rotation = remplacer la valeur à la main)${RESET}\n\n`);
140
- else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env.local est suivi par git — je n'y écris PAS de secrets.${RESET}\n ${DIM}Ajoute \`*.local\` au .gitignore (et \`git rm --cached .env.local\`), puis relance --write ;\n ou colle les lignes ci-dessous à la main :${RESET}\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n\n`);
159
+ else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env est suivi par git — je n'y écris PAS de secrets.${RESET}\n ${DIM}Ajoute \`.env\` au .gitignore (et \`git rm --cached .env\`), puis relance --write ;\n ou colle les lignes ci-dessous à la main :${RESET}\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n\n`);
141
160
  else if (opts.write) {
142
- const block = (dotenvLocal && !dotenvLocal.endsWith("\n") ? "\n" : "") + `# clés security — générées par \`nodefony security:secrets\`\n` + missingInDotenv.map((k) => `${k}=${secrets[k]}`).join("\n") + "\n";
143
- appendFileSync(path.resolve(this.#root(), ".env.local"), block);
144
- w(` ${GREEN}✓ écrit dans .env.local${RESET} ${DIM}(${missingInDotenv.join(", ")} — les clés déjà présentes n'ont pas été touchées)${RESET}\n\n`);
161
+ const block = (dotenv && !dotenv.endsWith("\n") ? "\n" : "") + `# clés security — générées par \`nodefony security:secrets\`\n` + missingInDotenv.map((k) => `${k}=${secrets[k]}`).join("\n") + "\n";
162
+ appendFileSync(path.resolve(this.#root(), ENV_FILE), block);
163
+ w(` ${GREEN}✓ écrit dans .env${RESET} ${DIM}(${missingInDotenv.join(", ")} — les clés déjà présentes n'ont pas été touchées)${RESET}\n\n`);
145
164
  } else w(` colle ces lignes ${DIM}(ou relance avec ${RESET}${CYAN}--write${RESET}${DIM} pour que je les écrive)${RESET} :\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n ${DIM}(en prod : Secret k8s / vault — jamais en git)${RESET}\n\n`);
146
165
  w(`${BOLD}2. Fichier ${CYAN}env.ts${RESET}${BOLD} — la déclaration typée${RESET} ${DIM}(env.ts est le seul lecteur de process.env)${RESET}\n`);
147
166
  if (missingInEnvTs.length === 0) w(` ${GREEN}✓ déjà déclarées${RESET}\n\n`);
@@ -150,13 +169,17 @@ var SecuritySecrets = class extends Command {
150
169
  if (missingInCfg.length === 0) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
151
170
  else if (cfgIsFragment) w(` complète le descripteur ${CYAN}securityConfig${RESET} :\n\n` + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n\n`);
152
171
  else w(" complète l'entrée security du manifeste modules :\n\n use(\"@nodefony/security\", {\n" + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n }),\n\n`);
153
- const jwtCable = /keystore\s*:/u.test(cfgTs);
154
- w(`${BOLD}4. Fichier ${CYAN}nodefony.config.ts${RESET}${BOLD} — les clés de SIGNATURE des jetons${RESET} ${DIM}(jwt.keystore)${RESET}\n`);
155
- if (jwtCable) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
156
- else w(` ${YELLOW}⚠ absentes : chaque process signe avec une clé ÉPHÉMÈRE.${RESET}\n ${DIM}Un jeton émis par la CLI porte alors un \`kid\` que le serveur ne\n connaît pas, et il est refusé — et un redémarrage invalide les jetons\n en vol. Ce n'est pas une valeur à coller : c'est une SOURCE à déclarer.${RESET}\n\n use("@nodefony/security", {\n jwt: { keystore: ctx.isProd ? {} : { dir: "var/keys" } },\n }),\n\n ${DIM}En production, le dossier n'a pas de sens (pods jetables) : la clé vient\n de l'environnement — jwt.keystore.keySetJson, injecté par ton gestionnaire\n de secrets et partagé par tous les pods.${RESET}\n\n`);
157
- w(`${DIM}Pourquoi 3 fichiers ? .env.local porte la VALEUR (secret machine, gitignoré —\nle .env commité ne porte que des défauts non-secrets) ; env.ts la DÉCLARE\n(catalogue typé, validé au boot) ; nodefony.config.ts la CÂBLE au module.\nLes étapes 2 et 3 ne se font qu'une fois — ensuite seule l'étape 1 vit.\nL'étape 4 sort de ce schéma, et c'est voulu : un keyset n'est pas une valeur\nqu'on colle, mais une source que le keystore gère (rotation, permissions).${RESET}\n\nRelance le serveur : plus aucun warning « clé ÉPHÉMÈRE » au boot.\n\n`);
172
+ const jwtDeclared = envTs.includes("NF_JWT_KEYSET");
173
+ const jwtWired = cfgTs.includes("NF_JWT_KEYSET");
174
+ w(`${BOLD}4. La clé de SIGNATURE des jetons${RESET} ${DIM}(NF_JWT_KEYSET — production seulement)${RESET}\n`);
175
+ if (jwtDeclared && jwtWired) w(` ${GREEN}✓ déclarée dans env.ts et câblée dans ${cfgRel}${RESET}\n`);
176
+ else {
177
+ if (!jwtDeclared) w(` ${CYAN}env.ts${RESET}, dans le defineEnv({ … }) :\n\n NF_JWT_KEYSET: envString({ optional: true }),\n\n`);
178
+ if (!jwtWired) w(` ${CYAN}${cfgRel}${RESET}, entrée security :\n\n jwt: {\n keystore: {\n keySetJson: ctx.env.NF_JWT_KEYSET,\n dir: ctx.isProd ? undefined : "var/keys",\n },\n },\n\n`);
179
+ }
180
+ w(` ${DIM}Développement : rien à poser, la clé vit dans var/keys/.\n Production : UNE valeur pour tous les pods et workers, générée une fois —\n ${RESET}${CYAN}npx nodefony security:secrets --jwt-keyset${RESET}${DIM} — puis rangée dans le\n gestionnaire de secrets et injectée en NF_JWT_KEYSET. Jamais dans .env,\n jamais dans git. STABLE : la changer refuse les jetons en vol ; pour une\n rotation, ajouter la nouvelle clé au jeu, la rendre « active », garder l'ancienne.${RESET}\n\n`);
181
+ w(`${DIM}Pourquoi 3 fichiers ? .env porte la VALEUR (secret du poste, gitignoré ;\nen production, le gestionnaire de secrets) ; env.ts la DÉCLARE\n(catalogue typé, validé au boot) ; ${cfgRel} la CÂBLE au module.\nLes étapes 2 et 3 ne se font qu'une fois — ensuite seule l'étape 1 vit.${RESET}\n\nRelance le serveur : plus aucun warning « clé ÉPHÉMÈRE » au boot.\n\n`);
158
182
  return this;
159
183
  }
160
184
  };
161
- //#endregion
162
185
  export { SecuritySecrets as default };
@@ -1,6 +1,6 @@
1
1
  import { AUTH_LOGIN_PATH, CONSOLE_DATA_RUN_PROFILE, Command, askPasswordMasked } from "nodefony";
2
2
  import { WeakPasswordError } from "@nodefony/user";
3
- //#region nodefony/command/security-user-add.ts
3
+ // nodefony/command/security-user-add.ts
4
4
  const options = {
5
5
  runProfile: CONSOLE_DATA_RUN_PROFILE,
6
6
  helpGroup: "COMPTES ET SECRETS",
@@ -42,7 +42,14 @@ const ROLE_BASE = "ROLE_USER";
42
42
  */
43
43
  var SecurityUserAdd = class extends Command {
44
44
  constructor(cli) {
45
- super("security:user:add", "crée un compte (mot de passe demandé masqué)", cli, options);
45
+ super("security:user:add", "crée un compte (mot de passe demandé masqué)", cli, {
46
+ ...options,
47
+ examples: [
48
+ "nodefony security:user:add alice",
49
+ "nodefony security:user:add admin --admin",
50
+ "nodefony security:user:add bob --roles ROLE_USER,ROLE_DEV"
51
+ ]
52
+ });
46
53
  this.addArgument("[identifier]", "identifiant (login) du compte");
47
54
  this.addOption("-p, --password <password>", "mot de passe (sinon : prompt masqué en TTY)");
48
55
  this.addOption("-r, --roles <roles>", "rôles CSV (ex: ROLE_USER,ROLE_DEV) — défaut ROLE_USER");
@@ -146,5 +153,4 @@ var SecurityUserAdd = class extends Command {
146
153
  return this;
147
154
  }
148
155
  };
149
- //#endregion
150
156
  export { SecurityUserAdd as default };
@@ -1,5 +1,5 @@
1
1
  import { CONSOLE_DATA_RUN_PROFILE, Command } from "nodefony";
2
- //#region nodefony/command/security-user-delete.ts
2
+ // nodefony/command/security-user-delete.ts
3
3
  const options = {
4
4
  runProfile: CONSOLE_DATA_RUN_PROFILE,
5
5
  helpGroup: "COMPTES ET SECRETS",
@@ -30,13 +30,17 @@ const ADMIN_ROLE = "ROLE_NODEFONY_ADMIN";
30
30
  * seul recours est une écriture directe en base. Le même garde-fou existe côté
31
31
  * data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
32
32
  *
33
- * La révocation des sessions et jetons du compte suit d'elle-même : la
34
- * suppression émet `onUserRevoked`, auquel `@nodefony/security` réagit en
35
- * éjectant sessions et jetons. Rien à faire de plus ici.
33
+ * La suppression passe par `users.delete`, qui émet `onUserRevoked`
34
+ * (`reason: "deleted"`) et ATTEND la cascade de `@nodefony/security` : sessions
35
+ * et jetons éjectés, secret TOTP et passkeys retirés. La commande n'émet rien
36
+ * elle-même — le service est le seul émetteur du cycle de vie d'un compte.
36
37
  */
37
38
  var SecurityUserDelete = class extends Command {
38
39
  constructor(cli) {
39
- super("security:user:delete", "supprime un compte, après confirmation", cli, options);
40
+ super("security:user:delete", "supprime un compte, après confirmation", cli, {
41
+ ...options,
42
+ examples: ["nodefony security:user:delete alice", "nodefony security:user:delete alice --yes"]
43
+ });
40
44
  this.addArgument("[identifier]", "identifiant (login) du compte à retirer");
41
45
  this.addOption("-y, --yes", "ne pas demander confirmation (obligatoire hors terminal)");
42
46
  }
@@ -95,9 +99,8 @@ var SecurityUserDelete = class extends Command {
95
99
  process.exitCode = 1;
96
100
  return this;
97
101
  }
98
- w(`${GREEN}✓ compte supprimé${RESET} — ${BOLD}${identifier}${RESET}\n${DIM} ses sessions et ses jetons sont révoqués (event onUserRevoked).${RESET}\n\n`);
102
+ w(`${GREEN}✓ compte supprimé${RESET} — ${BOLD}${identifier}${RESET}\n${DIM} ses sessions, ses jetons et ses facteurs (2FA, passkeys) sont retirés.${RESET}\n\n`);
99
103
  return this;
100
104
  }
101
105
  };
102
- //#endregion
103
106
  export { SecurityUserDelete as default };