@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
@@ -11,7 +11,16 @@ import { CliKernel, Command } from "nodefony";
11
11
  */
12
12
  export declare function ttlSeconds(raw: string | undefined): number | undefined | Error;
13
13
  /**
14
- * `nodefony security:token` — émet un jeton d'accès pour une porte de cette
14
+ * Durée demandée à l'émetteur : `--ttl` explicite, sinon 8 h quand le jeton
15
+ * part chez les agents, sinon rien — la configuration décide.
16
+ *
17
+ * @param raw - la valeur de `--ttl` telle que tapée, ou rien
18
+ * @param write - le jeton sera posé dans la configuration des agents
19
+ * @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
20
+ */
21
+ export declare function tokenTtlSeconds(raw: string | undefined, write: boolean): number | undefined | Error;
22
+ /**
23
+ * `nodefony security:mcp:token` — émet un jeton d'accès pour une porte de cette
15
24
  * application (la porte MCP par défaut).
16
25
  *
17
26
  * **Pourquoi une commande, et pas un `curl`.** Le jeton s'obtenait par un appel
@@ -25,11 +34,12 @@ export declare function ttlSeconds(raw: string | undefined): number | undefined
25
34
  * raison d'être de la liaison d'audience. La commande la vise d'elle-même,
26
35
  * `--resource` ne sert qu'à en viser une autre.
27
36
  *
28
- * Suit `security:secrets` : `--write` pose la valeur là où l'AGENT la lit
29
- * (gitignoré), jamais dans le `.env` commité, et ne remplace jamais une valeur
30
- * existante — une rotation est un geste explicite.
37
+ * `--write` pose la valeur là où l'AGENT la lit (gitignoré), jamais dans le
38
+ * `.env` de l'application. Relancée, elle REMPLACE le jeton chez les agents qui
39
+ * le portent déjà — la rotation est le geste le plus fréquent — et n'en câble
40
+ * aucun nouveau sans le proposer.
31
41
  */
32
- declare class SecurityToken extends Command {
42
+ declare class SecurityMcpToken extends Command {
33
43
  #private;
34
44
  constructor(cli: CliKernel);
35
45
  generate(identifierArg: string | undefined, opts: {
@@ -41,4 +51,4 @@ declare class SecurityToken extends Command {
41
51
  json?: boolean;
42
52
  }): Promise<this>;
43
53
  }
44
- export default SecurityToken;
54
+ export default SecurityMcpToken;
@@ -0,0 +1,23 @@
1
+ import { CliKernel, Command } from "nodefony";
2
+ /**
3
+ * `nodefony security:oauth:doctor` — dit POURQUOI un fournisseur OAuth refuse
4
+ * l'application, sans qu'un humain ait à se connecter.
5
+ *
6
+ * Trois verdicts distincts là où le journal ne montre qu'un 401 : l'émetteur
7
+ * se déclare autrement que la configuration ne l'écrit, l'URL de retour n'est
8
+ * pas enregistrée, le secret est faux. La même analyse alimente
9
+ * `nodefony doctor --live` (famille « Fournisseurs OAuth ») et l'endpoint
10
+ * d'administration `security/oauth/diagnosis` : une seule implémentation, celle
11
+ * du service `oauth2`.
12
+ *
13
+ * Valable pour tout fournisseur : les sondes passent par le fournisseur que le
14
+ * login construit, Keycloak n'y a rien de particulier.
15
+ */
16
+ declare class SecurityOAuthDoctor extends Command {
17
+ constructor(cli: CliKernel);
18
+ generate(opts: {
19
+ provider?: string;
20
+ json?: boolean;
21
+ }): Promise<this>;
22
+ }
23
+ export default SecurityOAuthDoctor;
@@ -19,6 +19,8 @@ declare class SecuritySecrets extends Command {
19
19
  generate(opts: {
20
20
  json?: boolean;
21
21
  write?: boolean;
22
+ jwtKeyset?: boolean;
23
+ env?: boolean;
22
24
  }): Promise<this>;
23
25
  }
24
26
  export default SecuritySecrets;
@@ -12,9 +12,10 @@ import { CliKernel, Command } from "nodefony";
12
12
  * seul recours est une écriture directe en base. Le même garde-fou existe côté
13
13
  * data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
14
14
  *
15
- * La révocation des sessions et jetons du compte suit d'elle-même : la
16
- * suppression émet `onUserRevoked`, auquel `@nodefony/security` réagit en
17
- * éjectant sessions et jetons. Rien à faire de plus ici.
15
+ * La suppression passe par `users.delete`, qui émet `onUserRevoked`
16
+ * (`reason: "deleted"`) et ATTEND la cascade de `@nodefony/security` : sessions
17
+ * et jetons éjectés, secret TOTP et passkeys retirés. La commande n'émet rien
18
+ * elle-même — le service est le seul émetteur du cycle de vie d'un compte.
18
19
  */
19
20
  declare class SecurityUserDelete extends Command {
20
21
  constructor(cli: CliKernel);
@@ -20,10 +20,10 @@ import { CliKernel, Command } from "nodefony";
20
20
  *
21
21
  * 🔒 **Les sessions et les jetons du compte sont RÉVOQUÉS**, et ce n'est pas une
22
22
  * option : on change un mot de passe parce qu'il est compromis ou perdu, et
23
- * laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé. La
24
- * révocation passe par l'événement que la suppression emploie déjà — une
25
- * cascade, un seul canal, et les abonnés futurs (webhooks) en profitent sans
26
- * rien changer ici.
23
+ * laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
24
+ * `UserService.changePassword` émet la révocation lui-même — la même que pour
25
+ * le plan d'administration et la page « mon mot de passe » : un seul émetteur,
26
+ * aucun transport ne peut l'oublier.
27
27
  */
28
28
  declare class SecurityUserPassword extends Command {
29
29
  constructor(cli: CliKernel);
@@ -13,6 +13,11 @@ declare const areaSchema: z.ZodObject<{
13
13
  host: z.ZodOptional<z.ZodString>;
14
14
  resource: z.ZodOptional<z.ZodString>;
15
15
  realtime: z.ZodDefault<z.ZodBoolean>;
16
+ subjectPolicy: z.ZodOptional<z.ZodEnum<{
17
+ ephemeral: "ephemeral";
18
+ require: "require";
19
+ }>>;
20
+ ephemeralRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
21
  }, z.core.$strict>;
17
22
  export declare const securityConfigSchema: z.ZodObject<{
18
23
  encoders: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -26,6 +31,7 @@ export declare const securityConfigSchema: z.ZodObject<{
26
31
  rounds: z.ZodDefault<z.ZodNumber>;
27
32
  }, z.core.$strict>>>;
28
33
  roleHierarchy: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
34
+ grantableScopes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
29
35
  areas: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
30
36
  pattern: z.ZodString;
31
37
  security: z.ZodDefault<z.ZodBoolean>;
@@ -40,6 +46,11 @@ export declare const securityConfigSchema: z.ZodObject<{
40
46
  host: z.ZodOptional<z.ZodString>;
41
47
  resource: z.ZodOptional<z.ZodString>;
42
48
  realtime: z.ZodDefault<z.ZodBoolean>;
49
+ subjectPolicy: z.ZodOptional<z.ZodEnum<{
50
+ ephemeral: "ephemeral";
51
+ require: "require";
52
+ }>>;
53
+ ephemeralRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
54
  }, z.core.$strict>>>;
44
55
  realtimeChannels: z.ZodDefault<z.ZodArray<z.ZodObject<{
45
56
  pattern: z.ZodString;
@@ -187,6 +198,7 @@ export declare const securityConfigSchema: z.ZodObject<{
187
198
  resourceServer: z.ZodDefault<z.ZodObject<{
188
199
  issuers: z.ZodDefault<z.ZodArray<z.ZodObject<{
189
200
  issuer: z.ZodString;
201
+ discoveryUrl: z.ZodOptional<z.ZodString>;
190
202
  jwksUri: z.ZodOptional<z.ZodString>;
191
203
  algorithms: z.ZodDefault<z.ZodArray<z.ZodEnum<{
192
204
  ES256: "ES256";
@@ -226,7 +238,7 @@ export declare const securityConfigSchema: z.ZodObject<{
226
238
  defaultRoles: z.ZodDefault<z.ZodArray<z.ZodString>>;
227
239
  allowSignup: z.ZodDefault<z.ZodBoolean>;
228
240
  successRedirect: z.ZodDefault<z.ZodString>;
229
- failureRedirect: z.ZodDefault<z.ZodString>;
241
+ failureRedirect: z.ZodOptional<z.ZodString>;
230
242
  providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
231
243
  clientId: z.ZodString;
232
244
  clientSecret: z.ZodString;
@@ -236,14 +248,65 @@ export declare const securityConfigSchema: z.ZodObject<{
236
248
  }>>;
237
249
  redirectUri: z.ZodString;
238
250
  issuer: z.ZodOptional<z.ZodString>;
251
+ discoveryUrl: z.ZodOptional<z.ZodString>;
239
252
  scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
240
253
  successRedirect: z.ZodOptional<z.ZodString>;
241
254
  failureRedirect: z.ZodOptional<z.ZodString>;
255
+ postLogoutRedirectUri: z.ZodOptional<z.ZodURL>;
242
256
  defaultRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
257
+ roleMapping: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
258
+ audiences: z.ZodOptional<z.ZodArray<z.ZodString>>;
259
+ allowPlatformRoles: z.ZodDefault<z.ZodBoolean>;
260
+ rolesSource: z.ZodOptional<z.ZodArray<z.ZodEnum<{
261
+ client: "client";
262
+ groups: "groups";
263
+ realm: "realm";
264
+ }>>>;
243
265
  label: z.ZodOptional<z.ZodString>;
266
+ icon: z.ZodOptional<z.ZodString>;
267
+ admin: z.ZodOptional<z.ZodObject<{
268
+ username: z.ZodString;
269
+ password: z.ZodString;
270
+ }, z.core.$strict>>;
244
271
  hidden: z.ZodDefault<z.ZodBoolean>;
245
272
  }, z.core.$strict>>>;
246
273
  }, z.core.$strict>>;
274
+ loginPage: z.ZodDefault<z.ZodObject<{
275
+ enabled: z.ZodDefault<z.ZodBoolean>;
276
+ path: z.ZodDefault<z.ZodString>;
277
+ title: z.ZodOptional<z.ZodString>;
278
+ logo: z.ZodOptional<z.ZodString>;
279
+ template: z.ZodOptional<z.ZodString>;
280
+ stylesheet: z.ZodOptional<z.ZodString>;
281
+ heading: z.ZodOptional<z.ZodString>;
282
+ subtitle: z.ZodOptional<z.ZodString>;
283
+ providersFirst: z.ZodDefault<z.ZodBoolean>;
284
+ hero: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
285
+ heading: z.ZodString;
286
+ text: z.ZodOptional<z.ZodString>;
287
+ }, z.core.$strict>]>>;
288
+ footer: z.ZodDefault<z.ZodBoolean>;
289
+ password: z.ZodDefault<z.ZodBoolean>;
290
+ skin: z.ZodDefault<z.ZodEnum<{
291
+ blueprint: "blueprint";
292
+ console: "console";
293
+ dots: "dots";
294
+ enterprise: "enterprise";
295
+ frontispiece: "frontispiece";
296
+ horizon: "horizon";
297
+ ledger: "ledger";
298
+ minimal: "minimal";
299
+ "photo-card": "photo-card";
300
+ }>>;
301
+ layout: z.ZodOptional<z.ZodEnum<{
302
+ bare: "bare";
303
+ card: "card";
304
+ split: "split";
305
+ }>>;
306
+ previewPicker: z.ZodDefault<z.ZodBoolean>;
307
+ previewHook: z.ZodOptional<z.ZodString>;
308
+ }, z.core.$strict>>;
309
+ allowInsecureCredentials: z.ZodDefault<z.ZodBoolean>;
247
310
  apiKeys: z.ZodDefault<z.ZodObject<{
248
311
  enabled: z.ZodDefault<z.ZodBoolean>;
249
312
  prefix: z.ZodDefault<z.ZodString>;
@@ -33,7 +33,7 @@ export interface IAuthenticator {
33
33
  /** Hook échec — log audit, throttling (J2)… Le 401 + challenge sont posés par le firewall. */
34
34
  onFailure(context: ContextType, error: Error): Promise<void>;
35
35
  /**
36
- * Challenge `WWW-Authenticate` (RFC 7235 : tout 401 DOIT en porter un).
36
+ * Challenge `WWW-Authenticate` (RFC 9110 §11 : tout 401 DOIT en porter un).
37
37
  * Le firewall pose celui du premier authenticator de la zone qui en déclare.
38
38
  * Ex. `'Basic realm="nodefony", charset="UTF-8"'`, `'Bearer'`.
39
39
  *
@@ -29,6 +29,23 @@ export interface IFirewallZoneDescription {
29
29
  host: string | null;
30
30
  /** Zone valable aussi pour les frames WebSocket (api.request + subscribe). */
31
31
  realtime: boolean;
32
+ /** Rôles exigés par défaut dans la zone (en OU) ; vide = toute identité authentifiée. */
33
+ roles?: readonly string[] | undefined;
34
+ /**
35
+ * Zone ouverte à tout compte que la connexion OAuth crée à la volée : sans
36
+ * rôle, avec `session`, et `oauth2.allowSignup` actif. Un constat, pas une
37
+ * faute — `roles: ["ROLE_USER"]` l'assume en une ligne.
38
+ */
39
+ openToSignup?: boolean | undefined;
40
+ /**
41
+ * Le constat et ses deux gestes, rédigés par le pare-feu — `null` quand la
42
+ * zone n'est pas ouverte. Un lecteur (console, `doctor`) les rend TELS
43
+ * QUELS : deux rédactions du même fait finiraient par se contredire.
44
+ */
45
+ openToSignupNotice?: {
46
+ message: string;
47
+ action: string;
48
+ } | null | undefined;
32
49
  }
33
50
  /** Un authenticator : disponible (registre de fabriques) et/ou monté (≥1 zone). */
34
51
  export interface IFirewallAuthenticatorDescription {
@@ -38,7 +55,7 @@ export interface IFirewallAuthenticatorDescription {
38
55
  mounted: boolean;
39
56
  /** Présent dans le registre de fabriques (utilisable en config). */
40
57
  available: boolean;
41
- /** Déclare un challenge `WWW-Authenticate` (RFC 7235). */
58
+ /** Déclare un challenge `WWW-Authenticate` (RFC 9110 §11). */
42
59
  challenge: boolean;
43
60
  }
44
61
  /** Défenses transverses résolues (CSRF/CORS/headers/throttle) — sans secret. */
@@ -111,10 +128,34 @@ export interface IRoleDescription {
111
128
  /** Tous les rôles atteignables transitivement (hors le rôle lui-même), triés. */
112
129
  inherits: string[];
113
130
  }
131
+ /**
132
+ * Table de correspondance des rôles d'UN fournisseur d'identité
133
+ * (`oauth2.providers.<nom>.roleMapping`) — des noms de rôles, rien d'autre de
134
+ * sa configuration.
135
+ */
136
+ export interface IProviderRoleMappingDescription {
137
+ /** Nom du fournisseur (`keycloak`). */
138
+ provider: string;
139
+ /** Où ses rôles sont lus (`client`, `realm`, `groups`). */
140
+ sources: string[];
141
+ /**
142
+ * Rôle du fournisseur → rôle de l'application, triés par rôle du
143
+ * fournisseur ; `platform` = rôle de plateforme (`ROLE_NODEFONY_*`).
144
+ */
145
+ mappings: {
146
+ from: string;
147
+ to: string;
148
+ platform: boolean;
149
+ }[];
150
+ /** `true` si la table peut accorder des rôles de plateforme. */
151
+ allowPlatformRoles: boolean;
152
+ }
114
153
  /** Hiérarchie de rôles : déclaration brute + résolution aplatie. */
115
154
  export interface IRoleHierarchyDescription {
116
155
  /** Déclaration directe (`{ ROLE_ADMIN: ["ROLE_USER"] }`). */
117
156
  hierarchy: Record<string, string[]>;
118
157
  /** Résolution transitive précalculée par le {@link RoleHierarchyWalker}. */
119
158
  roles: IRoleDescription[];
159
+ /** Rôles accordés par des fournisseurs d'identité — vide sans `roleMapping`. */
160
+ providerMappings: IProviderRoleMappingDescription[];
120
161
  }
@@ -27,6 +27,19 @@ export interface IIssuerPolicy {
27
27
  */
28
28
  readonly requireIssParameter: boolean;
29
29
  }
30
+ /**
31
+ * Ce que porte une déconnexion initiée par l'application
32
+ * (OpenID Connect RP-Initiated Logout 1.0 §2).
33
+ */
34
+ export interface ILogoutRequest {
35
+ /**
36
+ * ID token reçu au login (`id_token_hint`) : il dit au fournisseur QUELLE
37
+ * session fermer, et lui évite de demander une confirmation à l'utilisateur.
38
+ */
39
+ readonly idTokenHint: string;
40
+ /** Adresse de retour, enregistrée chez le fournisseur (`post_logout_redirect_uri`). */
41
+ readonly postLogoutRedirectUri: string;
42
+ }
30
43
  /**
31
44
  * Adaptateur d'**un fournisseur OAuth/OIDC**, façade UNIFORME au-dessus d'un client
32
45
  * OAuth 2.0 — masque les divergences entre fournisseurs derrière un contrat stable
@@ -79,4 +92,39 @@ export interface IOAuthProvider {
79
92
  * @throws Si le fournisseur ne renvoie pas d'identifiant stable.
80
93
  */
81
94
  fetchProfile(tokens: OAuth2Tokens): Promise<IOAuthProfile>;
95
+ /**
96
+ * Construit l'adresse de déconnexion chez le fournisseur (RP-Initiated
97
+ * Logout). ABSENTE quand le fournisseur n'en publie aucune (GitHub, OIDC sans
98
+ * `end_session_endpoint`) : la déconnexion reste alors locale, et l'ID token
99
+ * n'est pas conservé.
100
+ */
101
+ createLogoutURL?(request: ILogoutRequest): URL;
102
+ /**
103
+ * Vérifie un jeton de déconnexion reçu sur le canal arrière (OpenID Connect
104
+ * Back-Channel Logout 1.0 §2.6) et rend ce qu'il désigne. ABSENTE quand le
105
+ * fournisseur ne sait pas en émettre (GitHub, OAuth sans OpenID Connect).
106
+ *
107
+ * @param logoutToken - le paramètre `logout_token` reçu, brut.
108
+ * @returns les claims qui désignent les sessions à fermer, ou `null` si le
109
+ * jeton est refusé (signature, émetteur, audience, expiration, forme).
110
+ * @throws Error si la vérification est IMPOSSIBLE (jeu de clés injoignable) —
111
+ * une panne n'est pas un jeton invalide.
112
+ */
113
+ verifyLogoutToken?(logoutToken: string): Promise<ILogoutTokenClaims | null>;
114
+ }
115
+ /**
116
+ * Ce qu'un jeton de déconnexion VÉRIFIÉ désigne (OpenID Connect Back-Channel
117
+ * Logout 1.0 §2.4) : au moins l'un de `subject` et `sid`.
118
+ */
119
+ export interface ILogoutTokenClaims {
120
+ /** Émetteur canonique — celui de la liste fermée qui a servi à vérifier. */
121
+ readonly issuer: string;
122
+ /** `sub` : l'utilisateur chez le fournisseur, ou `null`. */
123
+ readonly subject: string | null;
124
+ /** `sid` : LA session du fournisseur à fermer, ou `null` (= toutes celles de `subject`). */
125
+ readonly sid: string | null;
126
+ /** `jti` : identifiant unique du jeton, la clé de l'anti-rejeu. */
127
+ readonly tokenId: string;
128
+ /** `exp` : fin de validité, en secondes depuis l'époque. */
129
+ readonly expiresAt: number;
82
130
  }
@@ -45,6 +45,28 @@ export interface ISecuredArea extends ISecurityZone {
45
45
  * n'en ont pas besoin (session, mot de passe, jetons maison).
46
46
  */
47
47
  readonly resource?: string | undefined;
48
+ /**
49
+ * Politique de rattachement du sujet d'un jeton TIERS propre à cette zone,
50
+ * ou `undefined` pour suivre `resourceServer.subjectPolicy`.
51
+ *
52
+ * C'est ce qui fait cohabiter une zone d'appelants machine (`ephemeral`) et
53
+ * une zone de personnes (`require`) dans la même application.
54
+ */
55
+ readonly subjectPolicy?: "require" | "ephemeral" | undefined;
56
+ /**
57
+ * Rôles accordés par cette zone à un appelant `ephemeral` — définis dès que
58
+ * la zone déclare `subjectPolicy: "ephemeral"` (vide si elle n'en écrit
59
+ * aucun : un pouvoir local ne s'hérite jamais de la politique globale).
60
+ */
61
+ readonly ephemeralRoles?: readonly string[] | undefined;
62
+ /**
63
+ * Page de connexion vers laquelle le pare-feu conduit un visiteur anonyme
64
+ * qui NAVIGUE (`302` vers `<entryPoint>?from=<chemin demandé>`), au lieu du
65
+ * `401`. Un appel d'API, un POST ou un WebSocket gardent leur `401` : un
66
+ * client qui attend du JSON casserait sur une page HTML. Omis = `401` pour
67
+ * tous. Chemin de l'origine, vérifié au boot.
68
+ */
69
+ readonly entryPoint?: string | undefined;
48
70
  /**
49
71
  * Zone valable AUSSI pour le WebSocket (frames `api.request` + `subscribe`),
50
72
  * pas seulement HTTP. Défaut `true` (Zero Trust : une zone protégée ferme TOUS
@@ -8,7 +8,10 @@ import type { TotpAlgorithm } from "../src/totp/totpCrypto.js";
8
8
  * API). Les codes de récupération, eux, sont **hachés** (verify-only).
9
9
  */
10
10
  export interface ITotpSecret {
11
- /** Identifiant de l'utilisateur propriétaire (clé naturelle). */
11
+ /**
12
+ * Propriétaire = `IUser.identifier` (clé naturelle), jamais `IUser.id` —
13
+ * cf `ITotpSecretStore`.
14
+ */
12
15
  readonly userId: string;
13
16
  /**
14
17
  * Secret partagé `K` **chiffré** (blob opaque : `iv.tag.ciphertext` base64url).
@@ -60,7 +60,13 @@ export interface TotpSecretUpdate {
60
60
  * d'`IWebAuthnCredentialStore` / `ITokenStore` : le builtin `memory` est sans
61
61
  * dépendance, les adapters lourds s'enregistrent depuis leur propre module.
62
62
  *
63
- * Modèle **1 secret / utilisateur** (clé = `userId`) — `save` est un **upsert**.
63
+ * Modèle **1 secret / utilisateur** — `save` est un **upsert**.
64
+ *
65
+ * **Clé = `userId` = `IUser.identifier` — immuable, la même clé que la session et les jetons —,
66
+ * jamais `IUser.id`. Un appelant qui ne connaît que l'`id` (route admin
67
+ * `users/{id}/…`) résout d'abord le compte (`users.findById`), puis passe son
68
+ * `identifier`. Les deux sont des `string` : le compilateur ne voit pas l'erreur,
69
+ * et une clé fausse rend un succès qui n'a rien touché.
64
70
  */
65
71
  export interface ITotpSecretStore {
66
72
  /** Secret TOTP de l'utilisateur, ou `null` si non enrôlé. */
@@ -10,7 +10,10 @@
10
10
  export interface IWebAuthnCredential {
11
11
  /** Identifiant du credential (base64url) — unique, fourni par l'authenticator. */
12
12
  readonly id: string;
13
- /** Identifiant de l'utilisateur propriétaire (sub / userHandle applicatif). */
13
+ /**
14
+ * Propriétaire = `IUser.identifier` (sub / userHandle applicatif), jamais
15
+ * `IUser.id` — cf `IWebAuthnCredentialStore`.
16
+ */
14
17
  readonly userId: string;
15
18
  /** Clé publique **COSE** encodée base64url — vérifie la signature des assertions. */
16
19
  readonly publicKey: string;
@@ -67,6 +67,12 @@ export interface WebAuthnAuthUpdate {
67
67
  * persistance (mémoire / ORM / Redis). Convention-frère d'`ITokenStore` :
68
68
  * le builtin `memory` est sans dépendance, les adapters lourds s'enregistrent
69
69
  * depuis leur propre module (inversion de dépendance).
70
+ *
71
+ * **Clé de propriétaire = `userId` = `IUser.identifier` — immuable, la même clé que la session et les jetons —,
72
+ * jamais `IUser.id`. Un appelant qui ne connaît que l'`id` (route admin
73
+ * `users/{id}/…`) résout d'abord le compte (`users.findById`), puis passe son
74
+ * `identifier`. Les deux sont des `string` : le compilateur ne voit pas l'erreur,
75
+ * et une clé fausse rend un succès qui n'a rien touché.
70
76
  */
71
77
  export interface IWebAuthnCredentialStore {
72
78
  /** Credential par son id (base64url), ou `null` — résolution à l'authentification. */
@@ -5,6 +5,8 @@ import { nodefonyError } from "nodefony";
5
5
  * classes de `@nodefony/security`).
6
6
  *
7
7
  * - `400` — entrée invalide (nom vide, scope hors catalogue) ;
8
+ * - `403` — scope demandé que le porteur ne peut pas obtenir (`grantableScopes`,
9
+ * ou `admin:*` sans le rôle d'administration) ;
8
10
  * - `409` — plafond `apiKeys.maxPerSubject` atteint ;
9
11
  * - `503` — store de jetons indisponible (clés activées mais non provisionnées).
10
12
  *
@@ -12,6 +14,6 @@ import { nodefonyError } from "nodefony";
12
14
  * 401, message uniforme anti-énumération).
13
15
  */
14
16
  export declare class ApiKeyError extends nodefonyError {
15
- constructor(message: string, code: 400 | 409 | 503);
17
+ constructor(message: string, code: 400 | 403 | 409 | 503);
16
18
  }
17
19
  export default ApiKeyError;
@@ -0,0 +1,20 @@
1
+ import { nodefonyError } from "nodefony";
2
+ /**
3
+ * Secret refusé parce qu'il arrive en clair — `code = 403` (RFC 9110 §15.5.4 :
4
+ * le serveur a compris la requête mais refuse de l'honorer).
5
+ *
6
+ * Message constant ({@link INSECURE_TRANSPORT_MESSAGE}, cœur) : le navigateur
7
+ * le reconnaît, et il ne dit rien de la topologie.
8
+ *
9
+ * Levée AVANT toute vérification, quand un mot de passe ou un code de second
10
+ * facteur arrive en production sur une requête dont le scheme effectif n'est
11
+ * pas `https` (cf {@link CredentialTransportPolicy}). Ce n'est pas un 401 :
12
+ * aucune preuve n'a été jugée, et rien de ce que le client corrigera dans son
13
+ * secret ne changera la réponse — c'est le canal qu'il doit changer. Pas de
14
+ * `WWW-Authenticate` non plus : un défi Basic ferait rouvrir au navigateur la
15
+ * fenêtre qui renverrait le mot de passe par le même canal.
16
+ */
17
+ export declare class InsecureTransportError extends nodefonyError {
18
+ constructor();
19
+ }
20
+ export default InsecureTransportError;
@@ -6,3 +6,4 @@ export { InvalidTargetError } from "./InvalidTargetError.js";
6
6
  export { CsrfError } from "./CsrfError.js";
7
7
  export { SsrfError } from "./SsrfError.js";
8
8
  export { WebAuthnError } from "./WebAuthnError.js";
9
+ export { InsecureTransportError } from "./InsecureTransportError.js";
@@ -0,0 +1,154 @@
1
+ import type { LoginPageSkin } from "nodefony";
2
+ import type { IOAuthCheck } from "../src/oauth/providerDiagnosis.js";
3
+ import type { ISecurityConfig } from "../config/config.js";
4
+ /**
5
+ * Client de l'API d'administration de Keycloak — le point d'entrée UNIQUE de
6
+ * Nodefony vers cette API.
7
+ *
8
+ * Premier besoin : le thème de connexion d'un realm EXISTANT, que
9
+ * `security:keycloak:realm --write` ne peut pas changer (Keycloak ne relit le
10
+ * fichier d'import qu'à la création du realm). Les suivants — fermer une
11
+ * session Keycloak, lire un compte, exporter — s'ajoutent ici en méthodes, sur
12
+ * le même jeton et les mêmes gardes, au lieu de recopier l'appel.
13
+ *
14
+ * Il exige le compte d'administration du SERVEUR (realm `master`) : il est
15
+ * refusé hors développement, quoi que dise la configuration. Le service
16
+ * `keycloak` (`KeycloakService.admin()`) le rend, un par fournisseur.
17
+ *
18
+ * @module
19
+ */
20
+ /** Le compte d'administration du serveur Keycloak (realm `master`). */
21
+ export interface IKeycloakAdminCredentials {
22
+ readonly username: string;
23
+ readonly password: string;
24
+ }
25
+ /** Le serveur à joindre et le compte qui l'administre. */
26
+ export interface IKeycloakAdminTarget {
27
+ /** Émetteur du fournisseur — l'adresse du realm (`https://…/realms/<nom>`). */
28
+ readonly issuer: string;
29
+ readonly admin: IKeycloakAdminCredentials;
30
+ }
31
+ /** Ce que le client doit savoir. */
32
+ export interface IKeycloakAdminClientOptions extends IKeycloakAdminTarget {
33
+ /** Environnement du kernel : tout autre que `development` est refusé. */
34
+ readonly environment: string;
35
+ /** Transport (défaut : `fetch` global) — injecté par les tests. */
36
+ readonly fetch?: typeof globalThis.fetch | undefined;
37
+ /** Délai de chaque appel, en millisecondes (défaut 10 s). */
38
+ readonly timeoutMs?: number | undefined;
39
+ }
40
+ /**
41
+ * Issue de {@link KeycloakAdminClient.applyLoginTheme} :
42
+ * - `applied` : le thème a changé ;
43
+ * - `unchanged` : le realm portait déjà ce thème ;
44
+ * - `unmanaged` : le realm porte un thème qui n'est pas de Nodefony (ou
45
+ * aucun) — il appartient à l'application, rien n'est touché.
46
+ */
47
+ export type KeycloakLoginThemeOutcome = "applied" | "unchanged" | "unmanaged";
48
+ /** Ce que le geste a constaté et fait. */
49
+ export interface IKeycloakLoginThemeResult {
50
+ readonly realm: string;
51
+ /** Thème du realm AVANT le geste ; `null` s'il n'en déclarait aucun. */
52
+ readonly previous: string | null;
53
+ /** Thème que l'habillage demande. */
54
+ readonly theme: string;
55
+ readonly outcome: KeycloakLoginThemeOutcome;
56
+ }
57
+ /** Refus ou échec d'un appel ; le message nomme la cause et le geste suivant. */
58
+ export declare class KeycloakAdminError extends Error {
59
+ readonly status: number | null;
60
+ /**
61
+ * @param message - phrase destinée à l'opérateur
62
+ * @param status - statut HTTP de Keycloak, `null` s'il n'a pas répondu
63
+ */
64
+ constructor(message: string, status?: number | null);
65
+ }
66
+ /**
67
+ * Le realm et le compte d'administration d'un fournisseur, lus dans la
68
+ * configuration de sécurité VALIDÉE.
69
+ *
70
+ * @param config - configuration de sécurité (`defineSecurityConfig`)
71
+ * @param providerName - nom dans `oauth2.providers` (défaut `keycloak`)
72
+ * @returns l'émetteur et le compte
73
+ * @throws {KeycloakAdminError} Fournisseur absent, sans émetteur ou sans
74
+ * compte d'administration
75
+ */
76
+ export declare function keycloakAdminTarget(config: ISecurityConfig, providerName?: string): IKeycloakAdminTarget;
77
+ /**
78
+ * Client de l'API d'administration d'UN realm Keycloak.
79
+ *
80
+ * Le jeton `admin-cli` est demandé au premier appel puis GARDÉ jusqu'à son
81
+ * expiration ; un 401 en cours de route le renouvelle une fois. Les gardes
82
+ * (développement seulement, https seulement) sont tenues au constructeur :
83
+ * un client qui existe a le droit de parler.
84
+ */
85
+ export declare class KeycloakAdminClient {
86
+ #private;
87
+ /** Nom du realm administré. */
88
+ readonly realm: string;
89
+ /** Origine du serveur (`https://hôte:port`). */
90
+ readonly origin: string;
91
+ /**
92
+ * @param options - realm, compte, environnement, transport
93
+ * @throws {KeycloakAdminError} Hors développement, émetteur qui n'est pas un
94
+ * realm, ou émetteur en clair
95
+ */
96
+ constructor(options: IKeycloakAdminClientOptions);
97
+ /**
98
+ * Un appel à l'API d'administration du realm, jeton joint.
99
+ *
100
+ * @param method - verbe HTTP
101
+ * @param path - chemin SOUS `/admin/realms/<realm>` (`""` = le realm, `/users?…`)
102
+ * @param body - corps JSON, ou rien
103
+ * @returns la réponse brute — au consommateur de lire son statut
104
+ * @throws {KeycloakAdminError} Serveur injoignable ou compte refusé
105
+ */
106
+ request(method: string, path: string, body?: unknown): Promise<Response>;
107
+ /**
108
+ * La représentation du realm.
109
+ *
110
+ * @returns l'objet JSON rendu par Keycloak
111
+ * @throws {KeycloakAdminError} Realm introuvable ou lecture refusée
112
+ */
113
+ getRealm(): Promise<Record<string, unknown>>;
114
+ /**
115
+ * Change des champs du realm — et eux seuls : Keycloak ne touche pas aux
116
+ * champs absents d'un `PUT` partiel.
117
+ *
118
+ * @param changes - les champs à écrire
119
+ * @throws {KeycloakAdminError} Écriture refusée
120
+ */
121
+ updateRealm(changes: Record<string, unknown>): Promise<void>;
122
+ /**
123
+ * Pose sur le realm le thème de connexion qui porte un habillage de `/login`.
124
+ *
125
+ * Un realm habillé par l'application (thème ni `nodefony` ni `nodefony-*`,
126
+ * ou aucun) n'est JAMAIS modifié : même règle que la dérivation du fichier
127
+ * d'import.
128
+ *
129
+ * @param skin - habillage (`loginPage.skin`, ou celui d'un aperçu)
130
+ * @returns le thème d'avant, celui demandé, et ce qui a été fait
131
+ * @throws {KeycloakAdminError} Keycloak injoignable, compte refusé, realm
132
+ * introuvable
133
+ */
134
+ applyLoginTheme(skin: LoginPageSkin): Promise<IKeycloakLoginThemeResult>;
135
+ }
136
+ /**
137
+ * Le thème de connexion qu'une page servie par Keycloak charge.
138
+ *
139
+ * Lu sans aucun identifiant : la page d'autorisation publique charge ses
140
+ * fichiers sous `/resources/<version>/login/<thème>/…`.
141
+ *
142
+ * @param html - début de la page d'autorisation
143
+ * @returns le nom du thème, ou `null` si la page n'est pas celle de Keycloak
144
+ */
145
+ export declare function readKeycloakLoginTheme(html: string): string | null;
146
+ /**
147
+ * La sonde « thème » du diagnostic d'un fournisseur : la page de Keycloak
148
+ * suit-elle l'habillage de `/login` ?
149
+ *
150
+ * @param skin - habillage configuré (`loginPage.skin`)
151
+ * @returns une fonction qui juge la page d'autorisation ; elle rend `null` —
152
+ * aucune sonde — sur une page qui n'est pas de Keycloak
153
+ */
154
+ export declare function keycloakLoginThemeCheck(skin: LoginPageSkin): (html: string) => IOAuthCheck | null;