@nodefony/security 10.0.0-beta.3 → 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 (162) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +6 -8
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
  4. package/dist/nodefony/command/security-oauth-doctor.js +5 -3
  5. package/dist/nodefony/command/security-secrets.js +10 -4
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +5 -3
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +5 -3
  10. package/dist/nodefony/config/config.js +20 -4
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  14. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  15. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  16. package/dist/nodefony/errors/CsrfError.js +1 -2
  17. package/dist/nodefony/errors/InsecureTransportError.js +1 -2
  18. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  19. package/dist/nodefony/errors/SsrfError.js +1 -2
  20. package/dist/nodefony/errors/ThrottledError.js +1 -2
  21. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  22. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  23. package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
  24. package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
  25. package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
  26. package/dist/nodefony/keycloak/scaffold.js +3 -3
  27. package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
  28. package/dist/nodefony/scaffold/contribute.js +1 -2
  29. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  30. package/dist/nodefony/service/apiKeys.js +45 -5
  31. package/dist/nodefony/service/auditService.js +4 -5
  32. package/dist/nodefony/service/authFlow.js +10 -8
  33. package/dist/nodefony/service/authorization.js +1 -2
  34. package/dist/nodefony/service/cors.js +1 -2
  35. package/dist/nodefony/service/csrf.js +1 -2
  36. package/dist/nodefony/service/firewall.js +5 -4
  37. package/dist/nodefony/service/keycloak.js +1 -2
  38. package/dist/nodefony/service/oauth2.js +29 -7
  39. package/dist/nodefony/service/securityHeaders.js +1 -2
  40. package/dist/nodefony/service/tokenService.js +38 -22
  41. package/dist/nodefony/service/totp.js +4 -5
  42. package/dist/nodefony/service/webAuthn.js +4 -5
  43. package/dist/nodefony/service/webhooks.js +4 -5
  44. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  45. package/dist/nodefony/src/SecuredArea.js +1 -2
  46. package/dist/nodefony/src/accountBinding.js +96 -0
  47. package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
  48. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  49. package/dist/nodefony/src/admin/adminAudit.js +1 -2
  50. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  51. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  52. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  53. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  54. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  55. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  56. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  57. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  58. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  59. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  60. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
  61. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  62. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
  63. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
  64. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  65. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
  66. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
  67. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  68. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  69. package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
  70. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  71. package/dist/nodefony/src/csp.js +1 -2
  72. package/dist/nodefony/src/csrfToken.js +1 -2
  73. package/dist/nodefony/src/entryPoint.js +1 -2
  74. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  75. package/dist/nodefony/src/net/transportFailure.js +57 -0
  76. package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
  77. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  78. package/dist/nodefony/src/oauth/metadata.js +5 -16
  79. package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
  80. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
  81. package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
  82. package/dist/nodefony/src/oauth/providerRoles.js +1 -2
  83. package/dist/nodefony/src/oauth/providers/github.js +16 -6
  84. package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
  85. package/dist/nodefony/src/openToSignup.js +1 -2
  86. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  87. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  88. package/dist/nodefony/src/rolesWithin.js +57 -0
  89. package/dist/nodefony/src/sessionAuthentication.js +1 -2
  90. package/dist/nodefony/src/sessionIdentity.js +29 -3
  91. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  92. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  93. package/dist/nodefony/src/token/JwtKeystore.js +14 -5
  94. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  95. package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
  96. package/dist/nodefony/src/token/UserToken.js +1 -2
  97. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  98. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  99. package/dist/nodefony/src/token/secretFile.js +1 -2
  100. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  101. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  102. package/dist/nodefony/src/token/tokenSort.js +1 -2
  103. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  106. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  107. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  108. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  110. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
  111. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  112. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  113. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  114. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  115. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  116. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  117. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  118. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  119. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  120. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  124. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
  125. package/dist/types/nodefony/config/config.d.ts +1 -0
  126. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  127. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  128. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  129. package/dist/types/nodefony/service/oauth2.d.ts +20 -0
  130. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  131. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
  132. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
  133. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  134. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
  135. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  136. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  137. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  138. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  139. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  140. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  141. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  142. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  143. package/docs/api-keys.md +181 -174
  144. package/docs/audit.md +140 -140
  145. package/docs/authenticators.md +101 -101
  146. package/docs/authorization.md +116 -69
  147. package/docs/cors.md +64 -64
  148. package/docs/csrf.md +74 -71
  149. package/docs/external-jwt.md +22 -12
  150. package/docs/firewall.md +85 -82
  151. package/docs/headers.md +81 -81
  152. package/docs/index.md +4 -4
  153. package/docs/keycloak.md +95 -78
  154. package/docs/login-page.md +23 -23
  155. package/docs/oauth2.md +157 -146
  156. package/docs/obtenir-un-jeton.md +116 -26
  157. package/docs/tokens.md +128 -122
  158. package/docs/totp.md +136 -136
  159. package/docs/webauthn.md +153 -153
  160. package/docs/webhooks.md +127 -124
  161. package/package.json +13 -13
  162. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/keycloak.md CHANGED
@@ -36,8 +36,8 @@ source: "src/packages/@nodefony/security/docs/keycloak.md"
36
36
  > du realm suffit à le décrire. Le travail se passe surtout dans Keycloak : créer le realm, déclarer
37
37
  > un client **confidentiel**, enregistrer l'URL de retour au caractère près. Cette page fait le
38
38
  > chemin entier, puis montre comment le jeton Keycloak de la même personne ouvre aussi ton API, sur
39
- > le **même compte**. Ancré sur le fournisseur `createDiscoveredOidcProvider()` (`oidc.ts:267`) et
40
- > sur `OAuth2Service` (`oauth2.ts:280`).
39
+ > le **même compte**. Ancré sur le fournisseur `createDiscoveredOidcProvider()` (`oidc.ts#createDiscoveredOidcProvider`) et
40
+ > sur `OAuth2Service` (`oauth2.ts#OAuth2Service`).
41
41
 
42
42
  📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Keycloak**
43
43
 
@@ -98,19 +98,19 @@ Pour ton application, cela bloque trois risques concrets :
98
98
 
99
99
  **Aucun code propre à Keycloak.** Un serveur OpenID Connect publie lui-même ses points d'entrée ;
100
100
  le fournisseur `keycloak` les **découvre** à partir de l'émetteur, et c'est tout ce qu'il sait faire
101
- (`createDiscoveredOidcProvider()`, `oidc.ts:267`). Changer de realm, c'est changer une URL.
101
+ (`createDiscoveredOidcProvider()`, `oidc.ts#createDiscoveredOidcProvider`). Changer de realm, c'est changer une URL.
102
102
 
103
103
  **Une configuration fausse arrête le démarrage.** Le fournisseur `keycloak` est enregistré avec
104
- `requiresIssuer: true` (`oauthProviderRegistry.ts:137`). Au boot, un émetteur absent, ou qui n'est
104
+ `requiresIssuer: true` (`oauthProviderRegistry.ts#withKeycloakThemeHint`). Au boot, un émetteur absent, ou qui n'est
105
105
  pas une URL `https` sans requête ni fragment, lève une erreur qui nomme la clé
106
- (`checkProviderIssuer()`, `oauth2.ts:210`). Un Keycloak **éteint**, lui, ne bloque rien : le bouton
106
+ (`checkProviderIssuer()`, `oauth2.ts#checkProviderIssuer`). Un Keycloak **éteint**, lui, ne bloque rien : le bouton
107
107
  disparaît de l'écran de connexion et revient tout seul quand le realm répond à nouveau
108
- (`#isReachable()`, `oauth2.ts:830`).
108
+ (`#isReachable()`, `oauth2.ts#OAuth2Service.#isReachable`).
109
109
 
110
110
  **L'identité est la paire `(keycloak, sub)`, jamais l'email.** Au premier login, l'application crée
111
- un compte local lié à cette paire (`UserService.provisionOAuthUser()`, `UserService.ts:603`). Un
111
+ un compte local lié à cette paire (`UserService.provisionOAuthUser()`, `UserService.ts#UserService.provisionOAuthUser`). Un
112
112
  compte local qui porte déjà le même identifiant sans ce lien n'est **jamais** rattaché en silence :
113
- la connexion est refusée (`UserService.ts:414`).
113
+ la connexion est refusée (`UserService.ts#UserService.changePassword`).
114
114
 
115
115
  **Keycloak authentifie, l'application autorise.** Les rôles du realm ne sont pas lus : le compte
116
116
  reçoit `defaultRoles` à sa création, puis la base locale fait foi. Une élévation se fait dans
@@ -263,7 +263,7 @@ lui, en a un de test — Mailpit (`--profile keycloak`, interface `http://localh
263
263
  développement de l'application, et l'application **reconnaît d'office l'autorité qui l'a
264
264
  signé** : en développement, le service de certificats l'ajoute aux autorités de confiance du
265
265
  processus dès le démarrage, commandes en ligne comprises (`addDefaultCaCertificate()`,
266
- `certificates.ts:65`). La variable `NODE_EXTRA_CA_CERTS` n'est plus nécessaire.
266
+ `certificates.ts#addDefaultCaCertificate`). La variable `NODE_EXTRA_CA_CERTS` n'est plus nécessaire.
267
267
 
268
268
  Le realm généré porte aussi, comme celui du dépôt, un client **machine** `<app>-machine` (compte de
269
269
  service, `client_credentials`). Ses jetons portent l'audience de la zone `partner` que le gabarit
@@ -397,10 +397,42 @@ complète : sinon l'émetteur change selon qui demande, et l'application refuse
397
397
  Les trois valeurs entrent par `env.ts`, seul lecteur des variables d'environnement. Le fournisseur
398
398
  n'est déclaré que si les trois sont présentes : pas de bouton mort quand une variable manque.
399
399
 
400
- ```typescript
400
+ ```ts ignore
401
+ // nodefony/config/security.ts — le fragment du module, importé par le manifeste
402
+ import type { ConfigContext } from "nodefony";
403
+ import type { ISecurityConfigInput } from "@nodefony/security";
404
+ import type { env } from "../../env";
405
+
406
+ export const securityConfig = (ctx: ConfigContext<typeof env>) =>
407
+ ({
408
+ oauth2: {
409
+ // Rôles posés à la CRÉATION du compte local ; ceux du realm ne sont pas lus.
410
+ defaultRoles: ["ROLE_USER"],
411
+ providers: {
412
+ ...(ctx.env.NF_KEYCLOAK_ISSUER &&
413
+ ctx.env.NF_KEYCLOAK_CLIENT_ID &&
414
+ ctx.env.NF_KEYCLOAK_CLIENT_SECRET
415
+ ? {
416
+ keycloak: {
417
+ issuer: ctx.env.NF_KEYCLOAK_ISSUER,
418
+ clientId: ctx.env.NF_KEYCLOAK_CLIENT_ID,
419
+ clientSecret: ctx.env.NF_KEYCLOAK_CLIENT_SECRET,
420
+ redirectUri: `${ctx.env.NF_OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/keycloak/callback`,
421
+ // Libellé du bouton ; omis, il vaut « Keycloak ».
422
+ label: "Compte entreprise",
423
+ },
424
+ }
425
+ : {}),
426
+ },
427
+ },
428
+ }) satisfies ISecurityConfigInput;
429
+ ```
430
+
431
+ ```typescript ignore
401
432
  // env.ts — SEUL lecteur de process.env (catalogue typé, validé au boot).
402
433
  // nodefony.config.ts — `ctx.env` EST ce catalogue (typé par le paramètre générique).
403
434
  import { defineConfig, defineEnv, envString, use } from "nodefony";
435
+ import { securityConfig } from "./nodefony/config/security";
404
436
 
405
437
  export const env = defineEnv({
406
438
  // Émetteur = URL du realm, en https (la découverte et le contrôle de `iss` en partent).
@@ -415,28 +447,7 @@ export default defineConfig<typeof env>((ctx) => ({
415
447
  modules: [
416
448
  "@nodefony/http",
417
449
  "@nodefony/framework",
418
- use("@nodefony/security", {
419
- oauth2: {
420
- // Rôles posés à la CRÉATION du compte local ; ceux du realm ne sont pas lus.
421
- defaultRoles: ["ROLE_USER"],
422
- providers: {
423
- ...(ctx.env.NF_KEYCLOAK_ISSUER &&
424
- ctx.env.NF_KEYCLOAK_CLIENT_ID &&
425
- ctx.env.NF_KEYCLOAK_CLIENT_SECRET
426
- ? {
427
- keycloak: {
428
- issuer: ctx.env.NF_KEYCLOAK_ISSUER,
429
- clientId: ctx.env.NF_KEYCLOAK_CLIENT_ID,
430
- clientSecret: ctx.env.NF_KEYCLOAK_CLIENT_SECRET,
431
- redirectUri: `${ctx.env.NF_OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/keycloak/callback`,
432
- // Libellé du bouton ; omis, il vaut « Keycloak ».
433
- label: "Compte entreprise",
434
- },
435
- }
436
- : {}),
437
- },
438
- },
439
- }),
450
+ use("@nodefony/security", securityConfig(ctx)),
440
451
  ],
441
452
  }));
442
453
  ```
@@ -455,14 +466,16 @@ par une autorité qu'elle connaît. Trois cas :
455
466
  `NF_TLS_CA_FILE` alimente la clé `certificates.extraCaFiles` du module `@nodefony/http` : un ou
456
467
  plusieurs fichiers PEM, absolus ou relatifs à la racine de l'application, une chaîne entière
457
468
  (racine + intermédiaires) pouvant tenir dans un seul fichier. Ils sont ajoutés aux autorités du
458
- processus au démarrage, avant tout service (`trustCaFile()`, `certificates.ts:131`), et servent à
469
+ processus au démarrage, avant tout service (`trustCaFile()`, `certificates.ts#trustCaFile`), et servent à
459
470
  **tous** ses appels sortants : découverte, point de jeton, jeu de clés. Un fichier introuvable, ou
460
471
  sans certificat, **interrompt le démarrage** en nommant le chemin (code 78) — plutôt qu'un
461
- `fetch failed` au premier clic d'un utilisateur. C'est l'équivalent, porté par la configuration, de
472
+ `fetch failed` au premier clic d'un utilisateur. Seule exception : l'autorité de développement pas
473
+ encore engendrée est ignorée en mode `development` (le journal le dit) ; en production, elle doit
474
+ exister — `npx nodefony http:certificates` l'engendre. C'est l'équivalent, porté par la configuration, de
462
475
  `NODE_EXTRA_CA_CERTS` : même effet, sans syntaxe de shell à traduire sous Windows.
463
476
 
464
477
  Pourquoi la deuxième ligne du tableau : l'autorité de développement n'est reconnue d'office qu'en
465
- mode `development` (`Certificate.trustDevelopmentAuthority()`, `certificates.ts:555`). Lancée
478
+ mode `development` (`Certificate.trustDevelopmentAuthority()`, `certificates.ts#Certificate.trustDevelopmentAuthority`). Lancée
466
479
  en production — pour essayer le build sur le poste, par exemple —, l'application ne lui fait plus
467
480
  confiance sans qu'on le lui dise, et c'est voulu : une application de production qui adopterait en
468
481
  silence une autorité de développement ouvrirait une porte qu'aucune configuration ne montre. Le
@@ -548,14 +561,14 @@ keycloak: {
548
561
  1. Keycloak rend un jeton d'accès qui porte `"resource_access": { "mon-app": { "roles": ["admin"] } }`
549
562
  — c'est là, et seulement là par défaut, que Keycloak met les rôles (ni dans l'ID token, ni
550
563
  dans _userinfo_).
551
- 2. `mapProviderRoles()` (`providerRoles.ts:87`) traduit `admin` → `ROLE_ADMIN` par la table.
552
- 3. Le compte de bob reçoit `ROLE_ADMIN` (`reconcileProviderRoles()`, `user/nodefony/src/providerRoles.ts:112` côté
564
+ 2. `mapProviderRoles()` (`providerRoles.ts#mapProviderRoles`) traduit `admin` → `ROLE_ADMIN` par la table.
565
+ 3. Le compte de bob reçoit `ROLE_ADMIN` (`reconcileProviderRoles()`, `user/nodefony/src/providerRoles.ts#reconcileProviderRoles` côté
553
566
  `@nodefony/user`).
554
567
 
555
568
  **Les règles, à connaître avant de l'activer :**
556
569
 
557
570
  - **Recalcul à chaque connexion ET à chaque jeton d'API.** Un rôle retiré dans Keycloak disparaît
558
- au login suivant, et dès le jeton suivant sur l'API (`syncOAuthRoles()`, `UserService.ts:690`).
571
+ au login suivant, et dès le jeton suivant sur l'API (`syncOAuthRoles()`, `UserService.ts#UserService.syncOAuthRoles`).
559
572
  Une session déjà ouverte garde ses droits jusqu'à la reconnexion.
560
573
  - **Un rôle Keycloak absent de la table est ignoré**, jamais recopié tel quel : l'annuaire ne
561
574
  fabrique pas de droit que l'application n'a pas déclaré.
@@ -571,7 +584,7 @@ keycloak: {
571
584
  - **Rôles de plateforme (`ROLE_NODEFONY_*`) refusés par défaut.** Ils ouvrent la console
572
585
  d'administration, la génération de code, les secrets : les faire venir de Keycloak fait de
573
586
  l'administrateur du realm un administrateur de l'instance. La table qui en contient un refuse
574
- le démarrage (`config.ts:1243`) — sauf `allowPlatformRoles: true` ÉCRIT sur le fournisseur, que
587
+ le démarrage (`config.ts#oauthProviderSchema`) — sauf `allowPlatformRoles: true` ÉCRIT sur le fournisseur, que
575
588
  chaque démarrage rappelle par un avertissement. Sinon, ce rôle se donne à la main
576
589
  (`security:user:add --admin`, console d'administration).
577
590
 
@@ -597,31 +610,35 @@ coché.
597
610
  jetons :
598
611
 
599
612
  ```typescript ignore
600
- use("@nodefony/security", {
601
- resourceServer: {
602
- issuers: [
603
- // Les clés publiques du realm sont découvertes par l'émetteur.
604
- {
605
- issuer: "https://sso.example.com/realms/mon-entreprise",
606
- algorithms: ["RS256"],
613
+ // nodefony/config/security.ts
614
+ import type { ISecurityConfigInput } from "@nodefony/security";
615
+
616
+ export const securityConfig = () =>
617
+ ({
618
+ resourceServer: {
619
+ issuers: [
620
+ // Les clés publiques du realm sont découvertes par l'émetteur.
621
+ {
622
+ issuer: "https://sso.example.com/realms/mon-entreprise",
623
+ algorithms: ["RS256"],
624
+ },
625
+ ],
626
+ },
627
+ areas: {
628
+ api: {
629
+ pattern: "^/api",
630
+ authenticators: ["external-jwt"],
631
+ stateless: true,
632
+ // L'audience exigée : celle que le mapper inscrit dans le jeton.
633
+ resource: "https://api.example.com",
607
634
  },
608
- ],
609
- },
610
- areas: {
611
- api: {
612
- pattern: "^/api",
613
- authenticators: ["external-jwt"],
614
- stateless: true,
615
- // L'audience exigée : celle que le mapper inscrit dans le jeton.
616
- resource: "https://api.example.com",
617
635
  },
618
- },
619
- });
636
+ }) satisfies ISecurityConfigInput;
620
637
  ```
621
638
 
622
639
  **Le même compte, pas un second.** Quand l'émetteur d'un jeton est celui d'un fournisseur de
623
640
  connexion déclaré (`oauth2.providers.keycloak.issuer`), l'authenticator cherche d'abord le compte lié
624
- à `(keycloak, sub)` — celui que le login par navigateur a créé (`ExternalJwtAuthenticator.ts:403`).
641
+ à `(keycloak, sub)` — celui que le login par navigateur a créé (`ExternalSubjectResolver.ts#IExternalIssuerBinding.issuer`).
625
642
  Sans ce lien, le jeton désignerait un compte distinct, sous l'identifiant `<émetteur>#<sub>`.
626
643
 
627
644
  ## ☸️ En cluster — joindre Keycloak par son adresse interne
@@ -701,17 +718,17 @@ son adresse interne (constaté, pas déduit) :
701
718
  **Ce qui reste vérifié**, et ne se relâche jamais :
702
719
 
703
720
  - **L'émetteur est comparé à l'identique.** Seul le lieu de lecture change
704
- (`metadataCandidateUrls()`, `authorizationServer.ts:214`) : un document lu à l'adresse interne
721
+ (`metadataCandidateUrls()`, `authorizationServer.ts#metadataCandidateUrls`) : un document lu à l'adresse interne
705
722
  qui se déclare d'un autre émetteur est refusé.
706
723
  - **Le `http` n'est admis que sur l'origine EXACTE de `discoveryUrl`** — même schéma, même hôte,
707
724
  même port —, et seulement pour le point de jeton et le jeu de clés
708
- (`isBackChannelTransportAllowed()`, `authorizationServer.ts:240`). L'opérateur a déjà choisi
725
+ (`isBackChannelTransportAllowed()`, `authorizationServer.ts#isBackChannelTransportAllowed`). L'opérateur a déjà choisi
709
726
  cette adresse en l'écrivant ; un `http` vers une autre adresse est une adresse que personne n'a
710
727
  écrite, refusée.
711
728
  - **Les points du navigateur restent en `https`** (autorisation, déconnexion) : un navigateur
712
729
  renvoyé vers une adresse interne n'aboutirait nulle part.
713
730
  - **L'adresse est vérifiée au démarrage** : ni `http(s)`, ou porteuse d'identifiants ou d'un
714
- fragment → démarrage refusé (`parseDiscoveryUrl()`, `authorizationServer.ts:183`).
731
+ fragment → démarrage refusé (`parseDiscoveryUrl()`, `authorizationServer.ts#parseDiscoveryUrl`).
715
732
 
716
733
  > [!WARNING]
717
734
  > Une adresse interne en `http` fait passer le **secret du client** et les jetons **en clair** sur
@@ -723,7 +740,7 @@ son adresse interne (constaté, pas déduit) :
723
740
 
724
741
  - **`https` partout, même en développement.** L'ID token est lu sans vérifier sa signature, ce
725
742
  qu'OIDC Core §3.1.3.7 n'admet que parce qu'il arrive du point de jeton par TLS. Un émetteur en
726
- `http` est refusé au démarrage (`canonicalIssuer()`, `authorizationServer.ts:76`).
743
+ `http` est refusé au démarrage (`canonicalIssuer()`, `authorizationServer.ts#canonicalIssuer`).
727
744
  - **Client confidentiel obligatoire** : le schéma exige un secret non vide. Un client public ne
728
745
  conviendrait qu'à une application sans serveur, ce que n'est pas une application Nodefony.
729
746
  - **Anti-mix-up** : le paramètre `iss` du retour et le claim `iss` de l'ID token doivent égaler
@@ -760,27 +777,27 @@ les mêmes échecs (famille « Fournisseurs OAuth »), l'administration les sert
760
777
  `/nodefony/security/api/oauth/diagnosis`. Côté Keycloak, la sonde ouvre une session de connexion
761
778
  vide, qui expire seule.
762
779
 
763
- | Symptôme | Cause | Correction |
764
- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
765
- | Démarrage refusé : `security.oauth2.providers.keycloak.issuer est requis` | Les trois variables ne sont pas posées ensemble, ou `issuer` manque à la main | Poser `NF_KEYCLOAK_ISSUER` (`checkProviderIssuer()`, `oauth2.ts:210`) |
766
- | Démarrage refusé : `émetteur invalide … https` | Émetteur en `http`, ou avec `?…`/`#…` | Recopier l'`issuer` du document _OpenID Endpoint Configuration_ |
767
- | Pas de bouton + WARNING `oauth2 provider "keycloak" indisponible` | Keycloak injoignable, ou certificat non reconnu (`fetch failed (SELF_SIGNED_CERT_IN_CHAIN …)`) | Démarrer Keycloak ; déclarer l'autorité d'entreprise par `NF_TLS_CA_FILE`. Retour sous 30 s |
768
- | Démarrage refusé (78) : `http.certificates.extraCaFiles : « … » est illisible` | Le fichier de `NF_TLS_CA_FILE` n'existe pas à ce chemin (relatif = à la racine de l'application) | Corriger le chemin, ou monter le fichier dans le pod |
769
- | Avec `discoveryUrl` : `le document déclare « http://keycloak.sso:8080/… » alors qu'il a été demandé à « https://sso… »` | `KC_HOSTNAME` absent : Keycloak prend le nom de l'adresse par laquelle on l'interroge | `KC_HOSTNAME` = l'URL publique COMPLÈTE |
770
- | Avec `discoveryUrl` : le bouton est là, mais le retour de connexion échoue en `fetch failed` vers l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` absent : Keycloak publie son point de jeton sur l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true` |
771
- | Avec `discoveryUrl` : `« token_endpoint » doit être en https (ou en http sur l'origine de discoveryUrl …)` | Keycloak publie une AUTRE origine que celle de `discoveryUrl` (hôte ou port réécrit par un intermédiaire) | Écrire dans `discoveryUrl` exactement l'origine que Keycloak publie |
772
- | `Invalid parameter: redirect_uri` sur la page Keycloak | `redirectUri` ≠ les **Valid redirect URIs** du client | Aligner schéma, hôte (`localhost` ≠ `127.0.0.1`), port et chemin |
773
- | Retour sur `failureRedirect` + WARNING `unauthorized_client` (secret faux) ou `invalid_client` (client inconnu) | Mauvais secret, client absent du realm, ou client public (_Client authentication OFF_) | Recopier le secret de l'onglet _Credentials_ ; passer le client en ON |
774
- | Retour sur `failureRedirect` + WARNING `OAuth issuer mismatch` | Keycloak joint par deux adresses différentes : l'émetteur varie | Fixer `KC_HOSTNAME` à l'URL publique complète |
775
- | Retour sur `failureRedirect` + WARNING `local account exists without a keycloak link` | Un compte local porte déjà cet identifiant (`UserService.ts:414`) — typiquement un `sub` changé après réimport du realm | Rattacher le compte explicitement, ou fixer l'`id` des utilisateurs dans l'export du realm |
776
- | API : `401` avec un jeton Keycloak valide | Audience absente du jeton : pas de mapper _Audience_ | `security:keycloak:realm --write` puis import partiel ; `aud` doit contenir la `resource` de la zone |
777
- | Session de l'application vivante après une déconnexion chez Keycloak ; Keycloak journalise `Some clients have not been logged out` | _Front channel logout_ ON, ou **Backchannel logout URL** absente ou injoignable depuis Keycloak | _Front channel logout_ OFF ; une URL que Keycloak joint (en développement Linux : serveur à l'écoute au-delà de la boucle locale) |
780
+ | Symptôme | Cause | Correction |
781
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
782
+ | Démarrage refusé : `security.oauth2.providers.keycloak.issuer est requis` | Les trois variables ne sont pas posées ensemble, ou `issuer` manque à la main | Poser `NF_KEYCLOAK_ISSUER` (`checkProviderIssuer()`, `oauth2.ts#checkProviderIssuer`) |
783
+ | Démarrage refusé : `émetteur invalide … https` | Émetteur en `http`, ou avec `?…`/`#…` | Recopier l'`issuer` du document _OpenID Endpoint Configuration_ |
784
+ | Pas de bouton + WARNING `oauth2 provider "keycloak" indisponible` | Keycloak injoignable, ou certificat non reconnu (`fetch failed (SELF_SIGNED_CERT_IN_CHAIN …)`) | Démarrer Keycloak ; déclarer l'autorité d'entreprise par `NF_TLS_CA_FILE`. Retour sous 30 s |
785
+ | Démarrage refusé (78) : `http.certificates.extraCaFiles : « … » est illisible` | Le fichier de `NF_TLS_CA_FILE` n'existe pas à ce chemin (relatif = à la racine de l'application) | Corriger le chemin, ou monter le fichier dans le pod |
786
+ | Avec `discoveryUrl` : `le document déclare « http://keycloak.sso:8080/… » alors qu'il a été demandé à « https://sso… »` | `KC_HOSTNAME` absent : Keycloak prend le nom de l'adresse par laquelle on l'interroge | `KC_HOSTNAME` = l'URL publique COMPLÈTE |
787
+ | Avec `discoveryUrl` : le bouton est là, mais le retour de connexion échoue en `fetch failed` vers l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` absent : Keycloak publie son point de jeton sur l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true` |
788
+ | Avec `discoveryUrl` : `« token_endpoint » doit être en https (ou en http sur l'origine de discoveryUrl …)` | Keycloak publie une AUTRE origine que celle de `discoveryUrl` (hôte ou port réécrit par un intermédiaire) | Écrire dans `discoveryUrl` exactement l'origine que Keycloak publie |
789
+ | `Invalid parameter: redirect_uri` sur la page Keycloak | `redirectUri` ≠ les **Valid redirect URIs** du client | Aligner schéma, hôte (`localhost` ≠ `127.0.0.1`), port et chemin |
790
+ | Retour sur `failureRedirect` + WARNING `unauthorized_client` (secret faux) ou `invalid_client` (client inconnu) | Mauvais secret, client absent du realm, ou client public (_Client authentication OFF_) | Recopier le secret de l'onglet _Credentials_ ; passer le client en ON |
791
+ | Retour sur `failureRedirect` + WARNING `OAuth issuer mismatch` | Keycloak joint par deux adresses différentes : l'émetteur varie | Fixer `KC_HOSTNAME` à l'URL publique complète |
792
+ | Retour sur `failureRedirect` + WARNING `local account exists without a keycloak link` | Un compte local porte déjà cet identifiant (`UserService.ts#UserService.changePassword`) — typiquement un `sub` changé après réimport du realm | Rattacher le compte explicitement, ou fixer l'`id` des utilisateurs dans l'export du realm |
793
+ | API : `401` avec un jeton Keycloak valide | Audience absente du jeton : pas de mapper _Audience_ | `security:keycloak:realm --write` puis import partiel ; `aud` doit contenir la `resource` de la zone |
794
+ | Session de l'application vivante après une déconnexion chez Keycloak ; Keycloak journalise `Some clients have not been logged out` | _Front channel logout_ ON, ou **Backchannel logout URL** absente ou injoignable depuis Keycloak | _Front channel logout_ OFF ; une URL que Keycloak joint (en développement Linux : serveur à l'écoute au-delà de la boucle locale) |
778
795
 
779
796
  | Démarrage refusé : `… est un rôle de PLATEFORME` | `roleMapping` traduit vers un `ROLE_NODEFONY_*` | Donner ce rôle à la main, ou écrire `allowPlatformRoles: true` en connaissance de cause |
780
797
  | Le rôle Keycloak n'apparaît pas dans l'application | Rôle du REALM alors que la source est `client`, ou rôle d'un autre client, ou session ouverte avant le changement | Créer le rôle sur le client de l'application, ou `rolesSource: ["realm"]` ; se reconnecter |
781
798
 
782
799
  La cause d'un retour sur `failureRedirect` n'est jamais montrée au navigateur ; elle est dans le
783
- journal du serveur, préfixée `oauth2 callback "keycloak"` (`OAuth2Controller.ts:213`).
800
+ journal du serveur, préfixée `oauth2 callback "keycloak"` (`OAuth2Controller.ts#OAuth2Controller.authorize`).
784
801
 
785
802
  ## 🧪 Tests & couverture
786
803
 
@@ -64,14 +64,14 @@ la personnaliser là où c'est sans risque : les textes, les couleurs, le conten
64
64
  ## La vision Nodefony
65
65
 
66
66
  - **Configuration dans `@nodefony/security`, rendu dans `@nodefony/framework`** : le premier décrit
67
- la page (`AuthFlow.describeLoginPage()`, `authFlow.ts:470`), le second la rend
68
- (`buildLoginPageView()`, `loginPage.ts:227`) et la sert (`LoginPageController`,
69
- `LoginPageController.ts:102`). Le contrat entre les deux, `ILoginPageDescription`, vit au cœur
70
- (`authRoutes.ts:262`).
67
+ la page (`AuthFlow.describeLoginPage()`, `authFlow.ts#AuthFlow.describeLoginPage`), le second la rend
68
+ (`buildLoginPageView()`, `loginPage.ts#buildLoginPageView`) et la sert (`LoginPageController`,
69
+ `LoginPageController.ts#LoginPageController`). Le contrat entre les deux, `ILoginPageDescription`, vit au cœur
70
+ (`authRoutes.ts#ILoginPageDescription`).
71
71
  - **Tout passe par du texte échappé** : aucune valeur de la configuration ni de la requête n'entre
72
72
  dans le balisage autrement que par `<%= %>`. L'image du panneau passe par votre feuille de style,
73
73
  jamais par une adresse injectée dans le HTML.
74
- - **Un seul déroulé** : le script de la page (`mountLoginPage()`, `mountLoginPage.ts:85`) s'appuie
74
+ - **Un seul déroulé** : le script de la page (`mountLoginPage()`, `mountLoginPage.ts#mountLoginPage`) s'appuie
75
75
  sur `NodefonyLogin`, le même client que vos propres écrans peuvent utiliser.
76
76
 
77
77
  ## 🚀 Démarrage rapide
@@ -119,7 +119,7 @@ redéfinissent pour les deux thèmes (voir « Pièges », plus bas) :
119
119
  ### Ce qu'on observe
120
120
 
121
121
  - `GET /login` rend la page, avec `Cache-Control: no-store` et une CSP
122
- `frame-ancestors 'none'` (`LoginPageController.ts:112`) ;
122
+ `frame-ancestors 'none'` (`LoginPageController.ts#LoginPageController.constructor`) ;
123
123
  - le titre de la carte vaut « Espace client », le panneau affiche votre accroche à la place de la
124
124
  vitrine Nodefony ;
125
125
  - votre feuille arrive en second `<link rel="stylesheet">`, après `login.css` : vos variables
@@ -136,7 +136,7 @@ et ne laisse que votre marque sur l'image du panneau.
136
136
 
137
137
  `password: false` retire le formulaire : il ne reste que les boutons des fournisseurs. Chaque
138
138
  bouton prend son image si vous la réglez (`oauth2.providers.<nom>.icon`, sinon la marque GitHub ou
139
- une clé) — `listDisplayProviders()` (`oauth2.ts:422`).
139
+ une clé) — `listDisplayProviders()` (`oauth2.ts#OAuth2Service.listDisplayProviders`).
140
140
 
141
141
  Le libellé suit ce que l'utilisateur connaît. Une marque grand public (GitHub, Google, Microsoft…)
142
142
  garde son nom. Un serveur d'identité d'organisation (Keycloak, un OIDC d'entreprise) se lit
@@ -148,7 +148,7 @@ une page de connexion.
148
148
 
149
149
  Par défaut, quand des fournisseurs sont proposés, leurs boutons passent avant le formulaire : la
150
150
  plupart des comptes d'une entreprise sont ceux de son annuaire. `providersFirst: false` inverse
151
- l'ordre (`loginPage.ts:255`).
151
+ l'ordre (`loginPage.ts#buildLoginPageView`).
152
152
 
153
153
  ### Situation 4 — votre propre page
154
154
 
@@ -217,7 +217,7 @@ export const acmeSkin = {
217
217
 
218
218
  **Trois feuilles, dans cet ordre** : `login.css` (le socle), la feuille de l'habillage
219
219
  (`/nodefony/security/login/skins/<skin>.css`, aucune pour `frontispiece`), puis la vôtre — vos
220
- variables l'emportent toujours (`loginPage.ts:210`).
220
+ variables l'emportent toujours (`loginPage.ts#resolveSkin`).
221
221
 
222
222
  **La photo vient de vous.** Aucun habillage n'en publie : une photo par défaut alourdirait chaque
223
223
  installation et finirait à l'identique dans toutes les applications. Une seule variable suffit,
@@ -233,7 +233,7 @@ une couleur pleine.
233
233
  ```
234
234
 
235
235
  **La mise en page suit l'habillage** : sans `layout`, c'est celle pour laquelle il est dessiné ;
236
- un `layout` écrit gagne (`authFlow.ts:495`).
236
+ un `layout` écrit gagne (`authFlow.ts#AuthFlow.describeLoginPage`).
237
237
 
238
238
  **Keycloak suit le même habillage** : chaque habillage a son thème de connexion Keycloak
239
239
  (`nodefony-<habillage>`), que `security:keycloak:realm --write` pose dans le realm d'après
@@ -243,7 +243,7 @@ couleurs et leur mise en page.
243
243
 
244
244
  **Comparer sans redémarrer** : en développement, `/login?skin=dots` sert la page sous un autre
245
245
  habillage, et `&layout=card` force la mise en page — le temps d'une requête, sur vos vrais
246
- réglages. En production ces paramètres sont ignorés (`loginPage.ts:207`) : la page servie est
246
+ réglages. En production ces paramètres sont ignorés (`loginPage.ts#resolveSkin`) : la page servie est
247
247
  celle de la configuration, et rien d'autre.
248
248
 
249
249
  ## 🏗️ Architecture interne
@@ -262,11 +262,11 @@ sequenceDiagram
262
262
  ```
263
263
 
264
264
  - **La route est montée au démarrage** par `mountLoginPage()` du framework, hors firewall
265
- (`bypassFirewall`, `LoginPageController.ts:156`) — sinon une application qui protège `^/`
265
+ (`bypassFirewall`, `LoginPageController.ts#mountLoginPage`) — sinon une application qui protège `^/`
266
266
  exigerait d'être connecté pour se connecter.
267
267
  - **Les fournisseurs sont relus à chaque requête** : l'un d'eux peut tomber ou revenir pendant que
268
268
  le serveur tourne, et un bouton qui mène à une erreur est retiré de l'offre.
269
- - **La destination `?from=`** repasse par `safeRedirectPath` (`loginPage.ts:231`) : une adresse
269
+ - **La destination `?from=`** repasse par `safeRedirectPath` (`loginPage.ts#buildLoginPageView`) : une adresse
270
270
  hors de l'origine retombe sur `/`.
271
271
  - **Les boutons de fournisseur portent `from` et `back`** : `back` désigne cette page, si bien que
272
272
  le « Retour » du fournisseur et un échec y ramènent, destination comprise, quel que soit
@@ -275,7 +275,7 @@ sequenceDiagram
275
275
  FIXE dans la zone de message ; une annulation n'en affiche aucun. La raison reçue n'est jamais
276
276
  recopiée dans la page.
277
277
 
278
- ## ⚙️ Configuration (schéma Zod `loginPageSchema`, `config.ts:1040`)
278
+ ## ⚙️ Configuration (schéma Zod `loginPageSchema`, `config.ts#loginPageSchema`)
279
279
 
280
280
  <!-- prettier-ignore -->
281
281
  | Option | Type · défaut | Effet |
@@ -286,17 +286,17 @@ sequenceDiagram
286
286
  | `logo` | string · logo Nodefony | Adresse du logo (chemin servi, ou URL autorisée par la CSP). |
287
287
  | `heading` | string · « Se connecter » | Titre de la carte et de l'onglet. |
288
288
  | `subtitle` | string · déduit | Ligne sous le titre ; par défaut, déduite des moyens proposés. |
289
- | `stylesheet` | string · — | Feuille chargée après `login.css` et l'habillage (`config.ts:1024`). |
290
- | `skin` | un des neuf · `frontispiece` | Habillage (`config.ts:1133`) — voir « Habillages ». |
291
- | `layout` | `split` · `card` · `bare` · celle de l'habillage | Mise en page : panneau et formulaire, carte centrée, formulaire seul (`config.ts:1139`). |
292
- | `hero` | `{ heading, text? }` · `false` · vitrine | Contenu du panneau (`config.ts:1105`) ; une clé inconnue est refusée au démarrage. |
293
- | `providersFirst` | boolean · `true` | Fournisseurs avant le formulaire (`config.ts:1099`). |
294
- | `footer` | boolean · `true` | Protections de la session et « Propulsé par Nodefony » (`config.ts:1121`). |
289
+ | `stylesheet` | string · — | Feuille chargée après `login.css` et l'habillage (`config.ts#studioSchema`). |
290
+ | `skin` | un des neuf · `frontispiece` | Habillage (`config.ts#loginPageSchema`) — voir « Habillages ». |
291
+ | `layout` | `split` · `card` · `bare` · celle de l'habillage | Mise en page : panneau et formulaire, carte centrée, formulaire seul (`config.ts#loginPageSchema`). |
292
+ | `hero` | `{ heading, text? }` · `false` · vitrine | Contenu du panneau (`config.ts#loginPageSchema`) ; une clé inconnue est refusée au démarrage. |
293
+ | `providersFirst` | boolean · `true` | Fournisseurs avant le formulaire (`config.ts#loginPageSchema`). |
294
+ | `footer` | boolean · `true` | Protections de la session et « Propulsé par Nodefony » (`config.ts#loginPageSchema`). |
295
295
  | `password` | boolean · `true` | `false` = fournisseurs seulement. |
296
296
  | `previewPicker` | boolean · `false` | En **développement** seulement : un sélecteur d'habillage et de mise en page posé dans un coin, qui recharge avec `?skin=` / `?layout=`. Sans effet en production. |
297
297
  | `previewHook` | chemin · — | Avec `previewPicker`, en **développement** : chemin de l'application prévenu en `POST` (`{ skin, layout }`, même origine) AVANT chaque rechargement de l'aperçu — pour qu'un tiers suive l'habillage choisi (le thème d'un Keycloak de développement). Un échec n'empêche pas le rechargement ; une autre origine est refusée au démarrage. |
298
298
  | `template` | string · — | Gabarit `.eta` de remplacement — dernier recours. |
299
- | `oauth2.providers.<nom>.icon` | string · icône du framework | Image du bouton du fournisseur (`config.ts:1296`). |
299
+ | `oauth2.providers.<nom>.icon` | string · icône du framework | Image du bouton du fournisseur (`config.ts#oauthProviderSchema`). |
300
300
 
301
301
  Toute la section est en `z.strictObject` : une clé mal orthographiée interrompt le démarrage en la
302
302
  nommant, au lieu d'être ignorée.
@@ -307,7 +307,7 @@ nommant, au lieu d'être ignorée.
307
307
  le script porte le nonce CSP de la requête.
308
308
  - **Mot de passe en clair refusé en production** : sur une requête qui n'arrive pas en HTTPS, la
309
309
  connexion répond 403 avant toute vérification, et la page affiche « doit passer par HTTPS » au
310
- lieu de « mot de passe incorrect » (`mountLoginPage.ts:150`). Derrière un proxy qui termine TLS,
310
+ lieu de « mot de passe incorrect » (`mountLoginPage.ts#mountLoginPage`). Derrière un proxy qui termine TLS,
311
311
  déclarez-le dans `trustProxy` ([authenticators](authenticators.md#mot-de-passe-en-clair--refusé-en-production)).
312
312
  - **Message d'échec uniforme** : un refus d'identifiants affiche un message fixe, jamais le texte du
313
313
  serveur — il ne dit pas si le compte existe.
@@ -317,7 +317,7 @@ nommant, au lieu d'être ignorée.
317
317
  ## ⚡ Performance & mémoire
318
318
 
319
319
  - Le gabarit est compilé **une fois par processus**, et un échec de chargement n'est pas mémorisé
320
- (`keepSuccess()`, `loginPage.ts:128`) : la page ne reste pas en 500 après une correction.
320
+ (`keepSuccess()`, `loginPage.ts#keepSuccess`) : la page ne reste pas en 500 après une correction.
321
321
  - Feuille et script sont servis en statique, versionnés par leur empreinte (`?v=`) : le navigateur
322
322
  les garde en cache jusqu'au prochain changement. Le script pèse 4 Ko.
323
323