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

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 (160) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +44 -10
  4. package/dist/nodefony/command/security-oauth-doctor.js +66 -0
  5. package/dist/nodefony/command/security-secrets.js +42 -25
  6. package/dist/nodefony/command/security-token.js +46 -15
  7. package/dist/nodefony/command/security-user-delete.js +5 -4
  8. package/dist/nodefony/command/security-user-password.js +4 -11
  9. package/dist/nodefony/config/config.js +80 -5
  10. package/dist/nodefony/contracts/index.js +1 -2
  11. package/dist/nodefony/errors/InsecureTransportError.js +24 -0
  12. package/dist/nodefony/errors/index.js +2 -1
  13. package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
  14. package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
  15. package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
  16. package/dist/nodefony/keycloak/scaffold.js +231 -0
  17. package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
  18. package/dist/nodefony/scaffold/contribute.js +20 -0
  19. package/dist/nodefony/service/authFlow.js +111 -11
  20. package/dist/nodefony/service/firewall.js +111 -21
  21. package/dist/nodefony/service/keycloak.js +209 -0
  22. package/dist/nodefony/service/oauth2.js +379 -26
  23. package/dist/nodefony/service/tokenService.js +57 -6
  24. package/dist/nodefony/service/totp.js +51 -6
  25. package/dist/nodefony/service/webAuthn.js +20 -2
  26. package/dist/nodefony/src/SecuredArea.js +14 -3
  27. package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
  28. package/dist/nodefony/src/admin/adminAudit.js +0 -1
  29. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  30. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
  31. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
  32. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
  33. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
  34. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
  35. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  36. package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
  37. package/dist/nodefony/src/entryPoint.js +39 -0
  38. package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
  39. package/dist/nodefony/src/oauth/metadata.js +36 -9
  40. package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
  41. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
  42. package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
  43. package/dist/nodefony/src/oauth/providerRoles.js +79 -0
  44. package/dist/nodefony/src/oauth/providers/github.js +32 -6
  45. package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
  46. package/dist/nodefony/src/openToSignup.js +47 -0
  47. package/dist/nodefony/src/sessionAuthentication.js +26 -0
  48. package/dist/nodefony/src/token/JwtKeystore.js +78 -46
  49. package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
  50. package/dist/nodefony/src/token/secretFile.js +59 -2
  51. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
  52. package/dist/types/index.d.ts +26 -6
  53. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  54. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  55. package/dist/types/nodefony/command/security-token.d.ts +9 -0
  56. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  57. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  58. package/dist/types/nodefony/config/config.d.ts +63 -1
  59. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  60. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  61. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  62. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  63. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  64. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  65. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  66. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  67. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  68. package/dist/types/nodefony/errors/index.d.ts +1 -0
  69. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  70. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  71. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  72. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  73. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  74. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  75. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  76. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  77. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  78. package/dist/types/nodefony/service/oauth2.d.ts +125 -9
  79. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  80. package/dist/types/nodefony/service/totp.d.ts +28 -4
  81. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  82. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  83. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
  84. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
  85. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
  86. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  87. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  88. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  89. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  90. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  91. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  92. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  93. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  94. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  95. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  96. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  97. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  98. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  99. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  100. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  101. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  102. package/docs/api-keys.md +17 -17
  103. package/docs/audit.md +16 -16
  104. package/docs/authenticators.md +60 -27
  105. package/docs/authorization.md +16 -16
  106. package/docs/cors.md +29 -29
  107. package/docs/csrf.md +35 -33
  108. package/docs/external-jwt.md +66 -4
  109. package/docs/firewall.md +57 -24
  110. package/docs/headers.md +30 -30
  111. package/docs/index.md +12 -4
  112. package/docs/keycloak.md +818 -0
  113. package/docs/login-page.md +361 -0
  114. package/docs/oauth2.md +236 -122
  115. package/docs/obtenir-un-jeton.md +18 -11
  116. package/docs/tokens.md +136 -85
  117. package/docs/totp.md +105 -64
  118. package/docs/webauthn.md +90 -87
  119. package/docs/webhooks.md +3 -3
  120. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  121. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  122. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  123. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  124. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  125. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  126. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  127. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  128. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  129. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  130. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  131. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  132. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  133. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  134. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  135. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  136. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  137. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  138. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  139. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  140. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  141. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  142. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  143. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  144. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  145. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  146. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  147. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  148. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  149. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  150. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  151. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  152. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  153. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  154. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  155. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  156. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  157. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  158. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  159. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  160. package/package.json +17 -15
@@ -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<{
@@ -40,6 +45,11 @@ export declare const securityConfigSchema: z.ZodObject<{
40
45
  host: z.ZodOptional<z.ZodString>;
41
46
  resource: z.ZodOptional<z.ZodString>;
42
47
  realtime: z.ZodDefault<z.ZodBoolean>;
48
+ subjectPolicy: z.ZodOptional<z.ZodEnum<{
49
+ ephemeral: "ephemeral";
50
+ require: "require";
51
+ }>>;
52
+ ephemeralRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
53
  }, z.core.$strict>>>;
44
54
  realtimeChannels: z.ZodDefault<z.ZodArray<z.ZodObject<{
45
55
  pattern: z.ZodString;
@@ -187,6 +197,7 @@ export declare const securityConfigSchema: z.ZodObject<{
187
197
  resourceServer: z.ZodDefault<z.ZodObject<{
188
198
  issuers: z.ZodDefault<z.ZodArray<z.ZodObject<{
189
199
  issuer: z.ZodString;
200
+ discoveryUrl: z.ZodOptional<z.ZodString>;
190
201
  jwksUri: z.ZodOptional<z.ZodString>;
191
202
  algorithms: z.ZodDefault<z.ZodArray<z.ZodEnum<{
192
203
  ES256: "ES256";
@@ -226,7 +237,7 @@ export declare const securityConfigSchema: z.ZodObject<{
226
237
  defaultRoles: z.ZodDefault<z.ZodArray<z.ZodString>>;
227
238
  allowSignup: z.ZodDefault<z.ZodBoolean>;
228
239
  successRedirect: z.ZodDefault<z.ZodString>;
229
- failureRedirect: z.ZodDefault<z.ZodString>;
240
+ failureRedirect: z.ZodOptional<z.ZodString>;
230
241
  providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
231
242
  clientId: z.ZodString;
232
243
  clientSecret: z.ZodString;
@@ -236,14 +247,65 @@ export declare const securityConfigSchema: z.ZodObject<{
236
247
  }>>;
237
248
  redirectUri: z.ZodString;
238
249
  issuer: z.ZodOptional<z.ZodString>;
250
+ discoveryUrl: z.ZodOptional<z.ZodString>;
239
251
  scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
240
252
  successRedirect: z.ZodOptional<z.ZodString>;
241
253
  failureRedirect: z.ZodOptional<z.ZodString>;
254
+ postLogoutRedirectUri: z.ZodOptional<z.ZodURL>;
242
255
  defaultRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
256
+ roleMapping: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
257
+ audiences: z.ZodOptional<z.ZodArray<z.ZodString>>;
258
+ allowPlatformRoles: z.ZodDefault<z.ZodBoolean>;
259
+ rolesSource: z.ZodOptional<z.ZodArray<z.ZodEnum<{
260
+ client: "client";
261
+ groups: "groups";
262
+ realm: "realm";
263
+ }>>>;
243
264
  label: z.ZodOptional<z.ZodString>;
265
+ icon: z.ZodOptional<z.ZodString>;
266
+ admin: z.ZodOptional<z.ZodObject<{
267
+ username: z.ZodString;
268
+ password: z.ZodString;
269
+ }, z.core.$strict>>;
244
270
  hidden: z.ZodDefault<z.ZodBoolean>;
245
271
  }, z.core.$strict>>>;
246
272
  }, z.core.$strict>>;
273
+ loginPage: z.ZodDefault<z.ZodObject<{
274
+ enabled: z.ZodDefault<z.ZodBoolean>;
275
+ path: z.ZodDefault<z.ZodString>;
276
+ title: z.ZodOptional<z.ZodString>;
277
+ logo: z.ZodOptional<z.ZodString>;
278
+ template: z.ZodOptional<z.ZodString>;
279
+ stylesheet: z.ZodOptional<z.ZodString>;
280
+ heading: z.ZodOptional<z.ZodString>;
281
+ subtitle: z.ZodOptional<z.ZodString>;
282
+ providersFirst: z.ZodDefault<z.ZodBoolean>;
283
+ hero: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
284
+ heading: z.ZodString;
285
+ text: z.ZodOptional<z.ZodString>;
286
+ }, z.core.$strict>]>>;
287
+ footer: z.ZodDefault<z.ZodBoolean>;
288
+ password: z.ZodDefault<z.ZodBoolean>;
289
+ skin: z.ZodDefault<z.ZodEnum<{
290
+ blueprint: "blueprint";
291
+ console: "console";
292
+ dots: "dots";
293
+ enterprise: "enterprise";
294
+ frontispiece: "frontispiece";
295
+ horizon: "horizon";
296
+ ledger: "ledger";
297
+ minimal: "minimal";
298
+ "photo-card": "photo-card";
299
+ }>>;
300
+ layout: z.ZodOptional<z.ZodEnum<{
301
+ bare: "bare";
302
+ card: "card";
303
+ split: "split";
304
+ }>>;
305
+ previewPicker: z.ZodDefault<z.ZodBoolean>;
306
+ previewHook: z.ZodOptional<z.ZodString>;
307
+ }, z.core.$strict>>;
308
+ allowInsecureCredentials: z.ZodDefault<z.ZodBoolean>;
247
309
  apiKeys: z.ZodDefault<z.ZodObject<{
248
310
  enabled: z.ZodDefault<z.ZodBoolean>;
249
311
  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. */
@@ -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;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Le realm Keycloak d'une application — construit, jamais recopié.
3
+ *
4
+ * Deux usages, UNE construction : la contribution du paquet à une application
5
+ * neuve (`nodefony/keycloak/scaffold.ts`, jouée par `create app` après
6
+ * l'installation) écrit son realm d'import, et `security:keycloak:realm` le
7
+ * réécrit depuis la configuration EFFECTIVE d'une application qui tourne. Le
8
+ * cœur n'en sait rien : Keycloak est l'affaire de ce paquet.
9
+ *
10
+ * Avant lui, deux realms écrits à la main — celui du dépôt et le gabarit de
11
+ * l'application générée — répétaient ce que disait la configuration
12
+ * (identifiant de client, URL de retour, audience). Ils avaient divergé : le
13
+ * realm généré était né sans audience, et tout jeton y était refusé.
14
+ *
15
+ * Fonctions PURES : aucune lecture de disque, aucun environnement.
16
+ *
17
+ * @module
18
+ */
19
+ /** Une valeur JSON — ce qu'un realm d'import peut contenir. */
20
+ export type TKeycloakJson = string | number | boolean | null | TKeycloakJson[] | {
21
+ [key: string]: TKeycloakJson;
22
+ };
23
+ /** Un objet JSON du realm (client, mapper, utilisateur…). */
24
+ export type TKeycloakObject = {
25
+ [key: string]: TKeycloakJson;
26
+ };
27
+ /** Un rôle à déclarer dans le realm. */
28
+ export interface IKeycloakRoleInput {
29
+ /** Nom du rôle CÔTÉ KEYCLOAK (la clé de `roleMapping`). */
30
+ readonly name: string;
31
+ /** Texte affiché dans la console Keycloak. */
32
+ readonly description?: string;
33
+ }
34
+ /** Un compte de démonstration semé dans le realm de développement. */
35
+ export interface IKeycloakUserInput {
36
+ /** Identifiant FIXÉ : laissé à Keycloak, il change à chaque réimport. */
37
+ readonly id: string;
38
+ readonly username: string;
39
+ readonly email: string;
40
+ readonly firstName: string;
41
+ readonly lastName: string;
42
+ /** Mot de passe de DÉVELOPPEMENT, public par nature. */
43
+ readonly password: string;
44
+ /** Rôles du client navigateur accordés à ce compte. */
45
+ readonly clientRoles?: readonly string[];
46
+ }
47
+ /** Le client « machine » (compte de service, `client_credentials`). */
48
+ export interface IKeycloakMachineInput {
49
+ readonly clientId: string;
50
+ /** Secret de développement ; `null` = laissé à Keycloak (production). */
51
+ readonly secret: string | null;
52
+ readonly name?: string;
53
+ readonly description?: string;
54
+ }
55
+ /** Tout ce que le realm dit de l'application. */
56
+ export interface IKeycloakRealmInput {
57
+ /** Nom du realm — le dernier segment de l'émetteur (`…/realms/<nom>`). */
58
+ readonly realm: string;
59
+ /** Titre affiché sur la page de connexion. */
60
+ readonly displayName?: string;
61
+ /** Identifiant du client navigateur (`oauth2.providers.<p>.clientId`). */
62
+ readonly clientId: string;
63
+ /** Secret du client ; `null` = laissé à Keycloak (production). */
64
+ readonly clientSecret: string | null;
65
+ readonly clientName?: string;
66
+ readonly clientDescription?: string;
67
+ /** URL de retour EXACTES (RFC 9700 : comparaison au caractère près). */
68
+ readonly redirectUris: readonly string[];
69
+ /** Où Keycloak a le droit de renvoyer le navigateur après déconnexion. */
70
+ readonly postLogoutRedirectUris: readonly string[];
71
+ /** Point de déconnexion par canal arrière ; `null` = aucun. */
72
+ readonly backchannelLogoutUrl: string | null;
73
+ /**
74
+ * Ressources pour lesquelles Keycloak émet des jetons à cette application :
75
+ * un mapper d'audience par entrée, sur chaque client. Vide = aucun mapper,
76
+ * et le jeton porte alors `aud: "account"`, que toute zone refuse.
77
+ */
78
+ readonly audiences: readonly string[];
79
+ /** Rôles du client navigateur (`rolesSource: ["client"]`). */
80
+ readonly clientRoles: readonly IKeycloakRoleInput[];
81
+ /** Rôles du realm (`rolesSource: ["realm"]`). */
82
+ readonly realmRoles: readonly IKeycloakRoleInput[];
83
+ /** Client machine ; `null` = aucun. */
84
+ readonly machine: IKeycloakMachineInput | null;
85
+ /** Thèmes à déclarer ; absent = ceux de Keycloak. */
86
+ readonly themes?: {
87
+ readonly login?: string;
88
+ readonly account?: string;
89
+ readonly email?: string;
90
+ readonly admin?: string;
91
+ };
92
+ /** Comptes de démonstration (développement seulement). */
93
+ readonly users?: readonly IKeycloakUserInput[];
94
+ }
95
+ /**
96
+ * Le rôle composite que Keycloak donne à tout compte CRÉÉ dans le realm —
97
+ * `manage-account`, `view-profile`, `offline_access`, `uma_authorization`.
98
+ *
99
+ * ⚠️ Un compte IMPORTÉ ne le reçoit pas : l'import n'accorde que les rôles
100
+ * qu'il liste. Sans lui, la console du compte rend 401 sur le profil même de
101
+ * l'utilisateur.
102
+ *
103
+ * @param realm - nom du realm
104
+ * @returns le nom du rôle par défaut
105
+ */
106
+ export declare function keycloakDefaultRole(realm: string): string;
107
+ /**
108
+ * Construit le realm d'import complet d'une application.
109
+ *
110
+ * @param input - ce que la configuration de l'application dit du realm
111
+ * @returns le realm, prêt à être sérialisé par {@link renderKeycloakRealm}
112
+ */
113
+ export declare function buildKeycloakRealm(input: IKeycloakRealmInput): TKeycloakObject;
114
+ /**
115
+ * Fond un realm dérivé de la config dans le realm d'import existant.
116
+ *
117
+ * Ce que la configuration DIT est réécrit : nom du realm, thèmes déclarés,
118
+ * client navigateur (flux, URL de retour, audience, déconnexion), client
119
+ * machine (flux, audience). Ce qu'un humain a écrit SURVIT : comptes,
120
+ * titre, noms et textes des clients, secret machine, rôles déjà déclarés,
121
+ * mappers qui ne sont pas d'audience, clients et clés inconnus. Régénérer
122
+ * ne détruit donc rien de ce que la console a ajouté.
123
+ *
124
+ * @param existing - le realm lu sur disque (JSON quelconque)
125
+ * @param derived - le realm construit par {@link buildKeycloakRealm}
126
+ * @returns le realm fusionné
127
+ * @throws Si `existing` n'est pas un objet JSON
128
+ */
129
+ export declare function mergeKeycloakRealm(existing: TKeycloakJson, derived: TKeycloakObject): TKeycloakObject;
130
+ /**
131
+ * Sérialise un realm comme le dépôt l'écrit : deux espaces, tableaux de
132
+ * valeurs simples sur une ligne quand ils tiennent en 80 colonnes, fin de
133
+ * ligne — la forme que prettier donne au fichier versionné.
134
+ *
135
+ * @param realm - le realm à écrire
136
+ * @returns le texte du fichier d'import
137
+ */
138
+ export declare function renderKeycloakRealm(realm: TKeycloakObject): string;