@nodefony/security 10.0.0-alpha.3 → 10.0.0-alpha.5

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 (36) hide show
  1. package/LICENSE +201 -543
  2. package/README.md +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
  5. package/dist/index.js +4 -2
  6. package/dist/nodefony/command/security-secrets.js +7 -3
  7. package/dist/nodefony/command/security-token.js +5 -5
  8. package/dist/nodefony/command/security-user-add.js +22 -6
  9. package/dist/nodefony/command/security-user-password.js +111 -0
  10. package/dist/nodefony/config/config.js +3 -1
  11. package/dist/nodefony/service/auditService.js +3 -3
  12. package/dist/nodefony/service/oauth2.js +79 -2
  13. package/dist/nodefony/service/tokenService.js +3 -3
  14. package/dist/nodefony/service/totp.js +3 -3
  15. package/dist/nodefony/service/webAuthn.js +3 -3
  16. package/dist/nodefony/service/webhooks.js +3 -3
  17. package/dist/nodefony/src/token/JwtKeystore.js +37 -1
  18. package/dist/types/nodefony/command/security-user-add.d.ts +16 -0
  19. package/dist/types/nodefony/command/security-user-password.d.ts +31 -0
  20. package/dist/types/nodefony/config/config.d.ts +2 -0
  21. package/dist/types/nodefony/service/oauth2.d.ts +41 -1
  22. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +20 -0
  23. package/docs/audit.md +5 -5
  24. package/docs/authenticators.md +10 -10
  25. package/docs/authorization.md +2 -2
  26. package/docs/cors.md +4 -4
  27. package/docs/csrf.md +5 -5
  28. package/docs/firewall.md +7 -7
  29. package/docs/headers.md +7 -7
  30. package/docs/index.md +24 -0
  31. package/docs/oauth2.md +54 -32
  32. package/docs/tokens.md +10 -9
  33. package/docs/totp.md +1 -1
  34. package/docs/webauthn.md +3 -3
  35. package/docs/webhooks.md +1 -1
  36. package/package.json +12 -12
package/docs/oauth2.md CHANGED
@@ -33,8 +33,8 @@ source: "src/packages/@nodefony/security/docs/oauth2.md"
33
33
  > connecté à **ton** application. Nodefony orchestre ce voyage avec la posture **OAuth 2.1**
34
34
  > (RFC 9700) : Authorization Code, PKCE, `state` anti-CSRF, `iss` anti-mix-up. Point clé :
35
35
  > **aucun jeton n'atteint le navigateur** — le retour produit une **session BFF**, exactement la même
36
- > qu'un login par mot de passe. Ancré sur `OAuth2Service` (`oauth2.ts:55`) et le controller BFF
37
- > `OAuth2Controller` (`OAuth2Controller.ts:81`).
36
+ > qu'un login par mot de passe. Ancré sur `OAuth2Service` (`oauth2.ts:116`) et le controller BFF
37
+ > `OAuth2Controller` (`OAuth2Controller.ts:89`).
38
38
 
39
39
  📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **OAuth2**
40
40
 
@@ -114,13 +114,13 @@ qui les ferme ici :
114
114
  de session opaque (`OAuth2Controller.ts:155`).
115
115
  - **Interception du `code`** — un `code` capté (log de proxy, historique, redirection ouverte) est
116
116
  échangeable par l'attaquant. _Fermé par **PKCE**_ : l'échange exige le `code_verifier` resté en
117
- session (`OAuth2Service.createAuthorization()`, `oauth2.ts:143-145`).
117
+ session (`OAuth2Service.createAuthorization()`, `oauth2.ts:232-238`).
118
118
  - **CSRF de login** — un tiers force ta victime à terminer **son** flux à lui : elle se retrouve
119
119
  connectée sur le compte de l'attaquant, qui lit ensuite ce qu'elle y dépose. _Fermé par le `state`_
120
120
  comparé au retour (`OAuth2Controller.callback()`, `OAuth2Controller.ts:137-145`).
121
121
  - **Mix-up d'IdP** — un `code` obtenu chez un fournisseur malveillant est présenté au callback d'un
122
122
  fournisseur de confiance. _Fermé par la vérification de l'`iss`_ (`oauth2.ts:170-174`) **et** par
123
- l'exigence « même fournisseur qu'à l'aller » côté controller (`OAuth2Controller.ts:142`).
123
+ l'exigence « même fournisseur qu'à l'aller » côté controller (`OAuth2Controller.ts:170`).
124
124
 
125
125
  > [!IMPORTANT]
126
126
  > Le fournisseur social te dit **qui** est la personne. Il ne te dit **rien** de ses droits.
@@ -146,14 +146,14 @@ c'est l'authenticator `session` qui identifie chaque requête, comme après un m
146
146
  **Coût nul quand on ne s'en sert pas.** Aucune dépendance tierce : le client OAuth 2.0 est écrit
147
147
  dans le module (`oauth2Client.ts:207`), et `jose` — seul recours externe, pour lire les claims de
148
148
  l'ID token — est importé **paresseusement**. Les fournisseurs sont construits au premier login puis
149
- mémoïsés (`OAuth2Service.#resolveProvider()`, `oauth2.ts:190`) : c'est là, une seule fois par
149
+ mémoïsés (`OAuth2Service.#resolveProvider()`, `oauth2.ts:290`) : c'est là, une seule fois par
150
150
  processus, que les points d'entrée d'un émetteur OIDC sont découverts. Les routes ne sont montées
151
151
  que si le service existe (`framework/index.ts:379`) : sans social login configuré, la surface HTTP
152
152
  est **404**, pas « désactivée ».
153
153
 
154
154
  Au boot, la config est validée et les fournisseurs configurés sont confrontés au registre : un nom
155
155
  inconnu produit un **WARNING, pas un échec fatal** — `OAuth2Service.#build()` confronte les noms
156
- configurés à `listOAuthProviders()` (`oauth2.ts:86-95`) et le
156
+ configurés à `listOAuthProviders()` (`oauth2.ts:135-155`) et le
157
157
  reste de l'application démarre, le bouton correspondant n'apparaît simplement pas.
158
158
 
159
159
  ## 🚀 Démarrage rapide
@@ -207,8 +207,8 @@ export default defineConfig<typeof env>((ctx) => ({
207
207
 
208
208
  ### Les routes sont FOURNIES — tu n'écris aucun controller
209
209
 
210
- `mountOAuth2Routes()` (`OAuth2Controller.ts:208`) monte trois routes sous
211
- `/nodefony/security/api/oauth2` (`OAuth2Controller.ts:187`), et **seulement si** le service `oauth2`
210
+ `mountOAuth2Routes()` (`OAuth2Controller.ts:234`) monte trois routes sous
211
+ `/nodefony/security/api/oauth2` (`OAuth2Controller.ts:234`), et **seulement si** le service `oauth2`
212
212
  est présent (`framework/index.ts:379`) :
213
213
 
214
214
  | Route | Rôle |
@@ -226,7 +226,7 @@ Ton écran de login n'a donc qu'un lien à poser :
226
226
  ```
227
227
 
228
228
  > [!WARNING]
229
- > Ces routes portent `bypassFirewall: true` (`OAuth2Controller.ts:236`) — elles **sont** le mécanisme
229
+ > Ces routes portent `bypassFirewall: true` (`OAuth2Controller.ts:262`) — elles **sont** le mécanisme
230
230
  > d'authentification : l'utilisateur est anonyme pendant tout l'aller-retour. Les protéger créerait
231
231
  > un interblocage (il faudrait être connecté pour pouvoir se connecter). La session anonyme ne porte
232
232
  > que `state`/`code_verifier`, et son ID est **régénéré** à la promotion.
@@ -252,7 +252,7 @@ curl -s -b /tmp/jar http://localhost:5151/nodefony/security/api/auth/me
252
252
 
253
253
  # 4) Ce que l'UI de login interroge pour n'afficher que des boutons vivants
254
254
  curl -s http://localhost:5151/nodefony/security/api/oauth2/providers
255
- # {"providers":["github"]}
255
+ # {"providers":[{"name":"github","label":"GitHub"}]}
256
256
  ```
257
257
 
258
258
  Séquence identique prouvée de bout en bout sur serveur réel par `oauth2-flow.test.ts` (6 cas).
@@ -261,11 +261,11 @@ Séquence identique prouvée de bout en bout sur serveur réel par `oauth2-flow.
261
261
 
262
262
  ### Étape 1 — `createAuthorization(provider)`
263
263
 
264
- `OAuth2Service.createAuthorization()` (`oauth2.ts:139`) fabrique trois choses :
264
+ `OAuth2Service.createAuthorization()` (`oauth2.ts:232`) fabrique trois choses :
265
265
 
266
266
  1. un **`state`** aléatoire (anti-CSRF) ;
267
267
  2. un **`code_verifier`** — **seulement si** le fournisseur pratique PKCE (`usesPkce`,
268
- `oauth2.ts:143-145`) ; `null` sinon (GitHub) ;
268
+ `oauth2.ts:235-237`) ; `null` sinon (GitHub) ;
269
269
  3. l'**URL d'autorisation** construite par l'adaptateur du fournisseur, avec les scopes effectifs
270
270
  (ceux de la config, sinon les scopes par défaut du fournisseur, `oauth2.ts:216`).
271
271
 
@@ -274,7 +274,7 @@ mémoire, `OAuth2Controller.ts:105-108`), puis redirige en 302.
274
274
 
275
275
  ### Étape 2 — le retour, validé avant tout appel réseau
276
276
 
277
- `OAuth2Controller.callback()` (`OAuth2Controller.ts:132`) travaille dans cet ordre, et l'ordre est la
277
+ `OAuth2Controller.callback()` (`OAuth2Controller.ts:150`) travaille dans cet ordre, et l'ordre est la
278
278
  défense :
279
279
 
280
280
  1. **lire l'état de session, puis l'invalider immédiatement** (`OAuth2Controller.ts:126-129`) — le
@@ -286,15 +286,15 @@ défense :
286
286
 
287
287
  ### Étape 3 — `exchangeAndProvision(provider, code, verifier, iss)`
288
288
 
289
- `OAuth2Service.exchangeAndProvision()` (`oauth2.ts:162`) enchaîne :
289
+ `OAuth2Service.exchangeAndProvision()` (`oauth2.ts:254`) enchaîne :
290
290
 
291
291
  1. **anti-mix-up** — si le fournisseur annonce un émetteur attendu, l'`iss` reçu doit correspondre,
292
292
  et un `iss` **absent** est un rejet, pas une tolérance (`oauth2.ts:170-174`) ;
293
293
  2. **échange** du `code` sur le canal serveur, avec le `code_verifier`
294
- (`validateAuthorizationCode`, `oauth2.ts:181`), puis lecture du profil (`fetchProfile`,
295
- `oauth2.ts:176`) ;
294
+ (`validateAuthorizationCode`, `oauth2.ts:256`), puis lecture du profil (`fetchProfile`,
295
+ `oauth2.ts:275`) ;
296
296
  3. **provisionnement** du Shadow User avec la politique effective — rôles par défaut surchargeables
297
- **par fournisseur** (`oauth2.ts:180-181`), `allowSignup` global (`oauth2.ts:182-185`).
297
+ **par fournisseur** (`oauth2.ts:279-280`), `allowSignup` global (`oauth2.ts:283`).
298
298
 
299
299
  Toute erreur de cette étape est convertie en **échec uniforme** par le controller (`302
300
300
  failureRedirect`, `OAuth2Controller.ts:157-160`) : le client ne distingue pas un `iss` invalide d'un
@@ -337,7 +337,7 @@ préfixée `provider:providerId` — jamais de collision entre fournisseurs (`Us
337
337
  `defaultRoles` s'applique **au moment du `create`** (`UserService.ts:348`). Un second login
338
338
  n'écrase rien : promouvoir quelqu'un dans ta base reste effectif, et modifier `defaultRoles` en
339
339
  config ne repeint pas les comptes existants. C'est la traduction de la règle « OAuth =
340
- authentification, pas autorisation » (`oauth2.ts:178-181`, `config.ts:822-827`).
340
+ authentification, pas autorisation » (`oauth2.ts:279-283`, `config.ts:1042-1046`).
341
341
 
342
342
  > [!TIP]
343
343
  > Un fournisseur social ne doit **jamais** figurer dans le chemin d'obtention d'un rôle privilégié.
@@ -347,7 +347,7 @@ authentification, pas autorisation » (`oauth2.ts:178-181`, `config.ts:822-827`)
347
347
  ### Brancher sa propre politique
348
348
 
349
349
  Le provisioner est le service `users` **s'il implémente la capability**, détecté par duck-typing
350
- (`OAuth2Service.#resolveProvisioner()`, `oauth2.ts:224-231`). S'il ne l'implémente pas, le login
350
+ (`OAuth2Service.#resolveProvisioner()`, `oauth2.ts:327-333`). S'il ne l'implémente pas, le login
351
351
  **échoue** — jamais de création silencieuse par défaut. Une application qui veut sa propre politique
352
352
  (quota d'inscriptions, allowlist de domaines e-mail, rattachement à un tenant) implémente
353
353
  `provisionOAuthUser()` sur son service `users` : le profil normalisé `IOAuthProfile`
@@ -478,17 +478,17 @@ que le mapping du profil. Exemple sans réseau dans le dépôt :
478
478
 
479
479
  ## ⚙️ Configuration
480
480
 
481
- Section `oauth2` du schéma Zod (`config.ts:990`), branchée sur la config du module
482
- (`config.ts:990`). Table dérivée du schéma — les défauts sont ceux du code.
481
+ Section `oauth2` du schéma Zod (`config.ts:1034`), branchée sur la config du module
482
+ (`config.ts:1149`). Table dérivée du schéma — les défauts sont ceux du code.
483
483
 
484
484
  | Option | Type | Défaut | Effet |
485
485
  | ----------------- | -------------------- | --------------- | ---------------------------------------------------------- |
486
486
  | `enabled` | booléen | `true` | Coupe le social login ; les routes ne montent pas. |
487
- | `defaultRoles` | liste de rôles | `["ROLE_USER"]` | Rôles du Shadow User **à la création** (`config.ts:989`). |
488
- | `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:1015`). |
487
+ | `defaultRoles` | liste de rôles | `["ROLE_USER"]` | Rôles du Shadow User **à la création** (`config.ts:1042`). |
488
+ | `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:1048`). |
489
489
  | `successRedirect` | chemin | `/` | Où revient l'utilisateur après succès. |
490
490
  | `failureRedirect` | chemin | `/login` | Où il revient après échec (uniforme, sans détail). |
491
- | `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1031`). |
491
+ | `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1064`). |
492
492
 
493
493
  Par fournisseur (`oauthProviderSchema`, `config.ts:948`) :
494
494
 
@@ -500,7 +500,7 @@ Par fournisseur (`oauthProviderSchema`, `config.ts:948`) :
500
500
  | `issuer` | OIDC self-hosted | Realm Keycloak ; ignoré par les IdP à endpoints fixes. |
501
501
  | `clientAuthMethod` | | Comment le client s'authentifie au point de jeton (RFC 6749 §2.3). Omis = `client_secret_basic`, ce que la RFC demande de préférer. Poser `client_secret_post` quand le serveur l'EXIGE — il le publie dans `token_endpoint_auth_methods_supported`. |
502
502
  | `scopes` | | Vide = scopes par défaut du fournisseur. |
503
- | `successRedirect` / `failureRedirect` / `defaultRoles` | | Surchargent le global **pour ce fournisseur** (`oauth2.ts:124-131`). |
503
+ | `successRedirect` / `failureRedirect` / `defaultRoles` | | Surchargent le global **pour ce fournisseur** (`oauth2.ts:218-222`). |
504
504
 
505
505
  Les surcharges par fournisseur permettent la cohabitation : un IdP de recette garde ses redirections
506
506
  et ses rôles pendant qu'un IdP de production pointe ailleurs.
@@ -510,7 +510,7 @@ et ses rôles pendant qu'un IdP de production pointe ailleurs.
510
510
  ### Les jetons du fournisseur ne sont pas conservés
511
511
 
512
512
  C'est un choix, et il a des conséquences à connaître. Les jetons obtenus à l'échange vivent dans la
513
- portée locale de l'échange (`validateAuthorizationCode` puis `fetchProfile`, `oauth2.ts:181-182`) :
513
+ portée locale de l'échange (`validateAuthorizationCode` puis `fetchProfile`, `oauth2.ts:274-275`) :
514
514
  ils ne sont ni retournés, ni mis en
515
515
  session, ni persistés. Le profil normalisé qui traverse le système n'en contient aucun
516
516
  (`IOAuthUserProvisioner.ts:8-10`).
@@ -556,7 +556,7 @@ ou détruire les sessions), pas chez le fournisseur.
556
556
  | --------------------------------- | ------------------------ | --------------------------------------------------------------------- |
557
557
  | Flux Authorization Code | RFC 6749 | `IOAuthProvider.validateAuthorizationCode()` (`IOAuthProvider.ts:85`) |
558
558
  | PKCE | RFC 7636 | `usesPkce` (`IOAuthProvider.ts:58`) · `oidc.ts:104-111` |
559
- | Sécurité OAuth (BCP 2.1) | RFC 9700 | `OAuth2Service` (`oauth2.ts:56`) · `oauth2Schema` (`config.ts:1001`) |
559
+ | Sécurité OAuth (BCP 2.1) | RFC 9700 | `OAuth2Service` (`oauth2.ts:116`) · `oauth2Schema` (`config.ts:1034`) |
560
560
  | Anti-mix-up (`iss`) | RFC 9207 | `issuerPolicy` (`IOAuthProvider.ts:61`) · `oauth2.ts:170-181` |
561
561
  | Callback en correspondance exacte | RFC 9700 §4 | `redirectUri` (`config.ts:958`) |
562
562
  | Claims d'identité OIDC | OpenID Connect Core | `fetchProfile()` du helper OIDC (`oidc.ts:127-145`) |
@@ -569,9 +569,31 @@ Flux **exclus** par posture 2.1, et donc absents du code : `implicit` (jeton en
569
569
  ## 📡 Observabilité — Studio
570
570
 
571
571
  L'écran de connexion de Studio consomme directement le data plane : il interroge
572
- `/nodefony/security/api/oauth2/providers` (`Login.tsx:341`) et n'affiche **que** les fournisseurs
573
- opérationnels zéro bouton mort. Le clic déclenche la redirection vers `authorize`
574
- (`Login.tsx:84`).
572
+ `/nodefony/security/api/oauth2/providers` et affiche **tout** ce que cette route lui rend — zéro
573
+ bouton mort, et zéro fournisseur légitime masqué. Le clic déclenche la redirection vers `authorize`.
574
+
575
+ C'est le SERVEUR qui décide de la liste et des libellés, parce qu'il est le seul à lire la
576
+ configuration. L'écran ne connaît que des icônes de marque, pour l'esthétique : un fournisseur
577
+ qu'il ne reconnaît pas reçoit une icône neutre et reste affiché. Filtrer côté écran sur une table
578
+ de marques masquerait précisément les fournisseurs qu'une application enregistre elle-même —
579
+ Keycloak, ou un OIDC d'entreprise.
580
+
581
+ **Retirer un bouton sans fermer le flux** — `hidden: true` sur un fournisseur :
582
+
583
+ ```ts
584
+ providers: {
585
+ "test-oidc": { /* … */ hidden: true }, // absent de l'écran…
586
+ }
587
+ ```
588
+
589
+ …mais `/authorize` continue de répondre `302` : **masquer n'est pas désactiver**. Les deux usages
590
+ sont une fixture de développement qui pointe vers un serveur fictif (le bouton serait mort), et un
591
+ fournisseur réservé à un point d'entrée particulier. Pour le désactiver vraiment, il faut le
592
+ retirer de la configuration.
593
+
594
+ **Le libellé** vient de `label`, sinon il est dérivé du nom de la clé (`keycloak` → « Keycloak »,
595
+ `oidc` → « OIDC », `mon-idp` → « Mon Idp ») : un écran de connexion ne montre jamais un identifiant
596
+ technique brut.
575
597
 
576
598
  Côté suivi, chaque login réussi produit un événement d'audit `auth` / `login.success` via
577
599
  `AuthFlow.establishSessionFor()` (`authFlow.ts:229-236`), consultable dans l'écran **Audit**. La
@@ -588,7 +610,7 @@ provisionné dans l'écran **Users**, avec ses rôles réels.
588
610
  | Symptôme | Cause (dans le code) | Correction |
589
611
  | ------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- |
590
612
  | `404` sur `…/oauth2/…` | Service `oauth2` absent (module non chargé / `enabled: false`) | Charger `@nodefony/security` et activer `oauth2` |
591
- | WARNING « inconnu du registre » au boot | Nom configuré sans fabrique (`oauth2.ts:86-95`) | `registerOAuthProvider()` au chargement du module, ou builtin |
613
+ | WARNING « inconnu du registre » au boot | Nom configuré sans fabrique (`oauth2.ts:149-155`) | `registerOAuthProvider()` au chargement du module, ou builtin |
592
614
  | `404` « Unknown provider » sur `authorize` | Le nom n'est pas dans `listProviders()` (`OAuth2Controller.ts:97`) | Vérifier le nom exact **et** la présence des secrets |
593
615
  | Bouton absent de l'écran de login | Secrets manquants → fournisseur non monté (spread conditionnel) | Renseigner `clientId`/`clientSecret` dans l'env |
594
616
  | `redirect_uri_mismatch` chez le fournisseur | `redirectUri` ≠ URL enregistrée, au caractère près (`config.ts:958`) | Aligner schéma, hôte, port et chemin `/…/{provider}/callback` |
@@ -596,7 +618,7 @@ provisionné dans l'écran **Users**, avec ses rôles réels.
596
618
  | Callback échoue au **deuxième** essai | `state` à usage unique, consommé (`OAuth2Controller.ts:126-129`) | Refaire le flux depuis `authorize` — comportement attendu |
597
619
  | `OAuth issuer mismatch` | `iss` reçu ≠ l'émetteur attendu (`oauth2.ts:170-181`) | Corriger `issuer` (Keycloak : URL exacte du realm) |
598
620
  | Keycloak : erreur dès le premier login | `issuer` absent en config (`oauthProviderRegistry.ts:89-93`) | Renseigner l'URL du realm |
599
- | « provisioning indisponible » | `users` n'implémente pas la capability (`oauth2.ts:224-231`) | Implémenter `provisionOAuthUser()` sur le service `users` |
621
+ | « provisioning indisponible » | `users` n'implémente pas la capability (`oauth2.ts:327-333`) | Implémenter `provisionOAuthUser()` sur le service `users` |
600
622
  | Profil connu refusé | `allowSignup: false` sans lien préexistant (`UserService.ts:363`) | Activer `allowSignup` ou lier le compte au préalable |
601
623
  | Doublon de compte pour un utilisateur existant | Aucune liaison auto par e-mail (choix de sécurité) | Rattacher explicitement, utilisateur connecté |
602
624
  | Rôle attendu absent après re-login | Rôles posés à la **création** seulement (`UserService.ts:348`) | Modifier les rôles en base ; `defaultRoles` ne réécrit rien |
package/docs/tokens.md CHANGED
@@ -277,24 +277,25 @@ la victime est déconnectée (signal visible) au lieu d'un vol silencieux indéf
277
277
 
278
278
  ## 🔐 Le keystore Ed25519 — la clé ne fuit pas, pas de secret « par défaut » en prod
279
279
 
280
- `JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts:97-128`), pensée
280
+ `JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts:151-187`), pensée
281
281
  pour ne jamais auto-générer une clé en clair silencieusement en prod :
282
282
 
283
283
  1. **env** — `keySetJson` (JWK Set injecté depuis le catalogue d'env) : prod cloud, secret géré
284
- hors-app, même clé sur tous les pods (`JwtKeystore.ts:100-106`).
284
+ hors-app, même clé sur tous les pods (`JwtKeystore.ts:154-160`).
285
285
  2. **fichier** — `dir/keyset.json`, généré si absent, écriture atomique tmp+rename en mode 600 —
286
- `#writeAtomic()` (`JwtKeystore.ts:208-217`) : opt-in dev/VPS mono-machine.
286
+ `#writeAtomic()` (`JwtKeystore.ts:272-280`) : opt-in dev/VPS mono-machine.
287
287
  3. **mémoire** — aucune source → clé **éphémère + WARNING** explicite : perdue au redémarrage =
288
- refresh invalidés, incohérente en cluster (`JwtKeystore.ts:121-127`).
288
+ refresh invalidés, incohérente en cluster (`JwtKeystore.ts:179-186`).
289
289
 
290
- Le JWKS servi par `getPublicJWKS()` (`JwtKeystore.ts:87-90`) est **public** : la composante privée
291
- `d` est retirée à l'import par `#importKeyset()` (`JwtKeystore.ts:156-158`, RFC 8037/7517) — c'est
292
- lui qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts:174`), jamais
293
- une clé venue du token. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts:93-95`).
290
+ Le JWKS servi par `getPublicJWKS()` est **public** — `JwtKeystore.ts:141-145`.
291
+ La composante privée `d` en est retirée à l'import, par liste BLANCHE de paramètres
292
+ (`#importKeyset()`, `JwtKeystore.ts:206-228`, RFC 8037/7517).
293
+ C'est ce JWKS qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts:174`),
294
+ jamais une clé venue du jeton. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts:147-149`).
294
295
 
295
296
  > [!WARNING]
296
297
  > **Race au 1ᵉʳ boot d'un cluster sans clé pré-provisionnée** : deux workers peuvent générer des
297
- > clés différentes — le dernier `rename` gagne (`JwtKeystore.ts:61-64`). En prod, provisionner
298
+ > clés différentes — le dernier `rename` gagne (`JwtKeystore.ts:272-280`). En prod, provisionner
298
299
  > `keySetJson` hors-bande élimine ce cas : c'est la source recommandée.
299
300
 
300
301
  ## 🧩 Le store pluggable — durable par défaut, jamais de faux durable silencieux
package/docs/totp.md CHANGED
@@ -452,7 +452,7 @@ La saisie est tolérante — casse et tirets ignorés à la normalisation (`totp
452
452
 
453
453
  ## ⚙️ Configuration et mises en situation
454
454
 
455
- La section `totp` du schéma Zod (`config.ts:1107`) — validée au boot, donc une valeur hors bornes
455
+ La section `totp` du schéma Zod (`config.ts:1138`) — validée au boot, donc une valeur hors bornes
456
456
  échoue **au démarrage**, pas au premier login :
457
457
 
458
458
  | Option | Type | Défaut | Effet |
package/docs/webauthn.md CHANGED
@@ -121,7 +121,7 @@ Trois partis pris assumés :
121
121
 
122
122
  ### 1. Les passkeys sont déjà actives — la config utile
123
123
 
124
- `passkeys.enabled` vaut `true` par défaut (`config.ts:1106`). Ce que tu déclares vraiment, c'est **ton
124
+ `passkeys.enabled` vaut `true` par défaut (`config.ts:1137`). Ce que tu déclares vraiment, c'est **ton
125
125
  domaine** : sans `rpId`, le service prend le domaine de l'app, et bascule sur `localhost` si c'est une
126
126
  adresse IP (un navigateur refuse une IP comme `rpId`, `webAuthn.ts:134`).
127
127
 
@@ -285,7 +285,7 @@ sequenceDiagram
285
285
 
286
286
  `WebAuthnService.generateRegistrationOptions()` (`webAuthn.ts:276`) construit le défi et les
287
287
  contraintes. **`excludeCredentials`** y liste les passkeys déjà enrôlées (`webAuthn.ts:291`) : le même
288
- authenticator ne peut pas s'inscrire deux fois. Dans `authenticatorSelection` (`webAuthn.ts:261`),
288
+ authenticator ne peut pas s'inscrire deux fois. Dans `authenticatorSelection` (`webAuthn.ts:299`),
289
289
  `authenticatorAttachment` n'est transmis **que** s'il vaut autre chose que `"any"` — `"any"` rend la
290
290
  main au navigateur, téléphone par QR compris.
291
291
 
@@ -354,7 +354,7 @@ exactement les porteurs à risque de verrouillage.
354
354
  ## ⚙️ Configuration
355
355
 
356
356
  Table dérivée du schéma Zod `passkeysSchema` (`config.ts:447`), monté sous la clé `passkeys`
357
- (`config.ts:1106`).
357
+ (`config.ts:1137`).
358
358
 
359
359
  | Option | Type | Défaut | Effet |
360
360
  | ------------------------- | ------------------------------------------ | ------------ | ------------------------------------------------------------------------------ |
package/docs/webhooks.md CHANGED
@@ -824,7 +824,7 @@ mention.
824
824
  | `listPage(query)` | Page d'endpoints (vue publique, sans secret) | `webhooks.ts:468` |
825
825
  | `countEndpoints(query)` | `COUNT` natif ; `-1` si le backend ne sait pas compter | `webhooks.ts:456` |
826
826
  | `getEndpoint(id)` | Un endpoint (vue publique) ou `null` | `webhooks.ts:510` |
827
- | `update(id, patch)` | `url`/`events`/`enabled`/`description`/`metadata` ; URL re-validée | `webhooks.ts:414` |
827
+ | `update(id, patch)` | `url`/`events`/`enabled`/`description`/`metadata` ; URL re-validée | `webhooks.ts:458` |
828
828
  | `setEnabled(id, bool)` | Révocation douce | `webhooks.ts:542` |
829
829
  | `rotateSecret(id)` | Nouveau secret ; l'ancien meurt immédiatement | `webhooks.ts:553` |
830
830
  | `revealSecret(id)` | Secret en clair (action sensible, à auditer par l'appelant) | `webhooks.ts:572` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/security",
3
- "version": "10.0.0-alpha.3",
3
+ "version": "10.0.0-alpha.5",
4
4
  "description": "Pare-feu applicatif par zones pour Nodefony : authentification, autorisation par rôles, protection CSRF, journal d'audit",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "main": "./dist/index.js",
@@ -47,26 +47,26 @@
47
47
  "@simplewebauthn/server": "14.0.1",
48
48
  "jose": "6.2.12",
49
49
  "tslib": "2.8.1",
50
- "zod": "^4.4.3"
50
+ "zod": "^4.6.1"
51
51
  },
52
52
  "devDependencies": {
53
- "@nodefony/framework": "^10.0.0-alpha.3",
54
- "@nodefony/http": "^10.0.0-alpha.3",
55
- "@nodefony/user": "^10.0.0-alpha.3",
56
- "@types/node": "26.4.1",
53
+ "@nodefony/framework": "^10.0.0-alpha.5",
54
+ "@nodefony/http": "^10.0.0-alpha.5",
55
+ "@nodefony/user": "^10.0.0-alpha.5",
56
+ "@types/node": "26.5.1",
57
57
  "@vitest/coverage-v8": "5.0.0",
58
- "nodefony": "^10.0.0-alpha.3",
58
+ "nodefony": "^10.0.0-alpha.5",
59
59
  "rimraf": "6.1.3",
60
60
  "vitest": "5.0.0"
61
61
  },
62
- "license": "CECILL-B",
62
+ "license": "Apache-2.0",
63
63
  "readmeFilename": "README.md",
64
64
  "contributors": [],
65
65
  "peerDependencies": {
66
- "@nodefony/framework": "^10.0.0-alpha.3",
67
- "@nodefony/http": "^10.0.0-alpha.3",
68
- "@nodefony/user": "^10.0.0-alpha.3",
69
- "nodefony": "^10.0.0-alpha.3"
66
+ "@nodefony/framework": "^10.0.0-alpha.5",
67
+ "@nodefony/http": "^10.0.0-alpha.5",
68
+ "@nodefony/user": "^10.0.0-alpha.5",
69
+ "nodefony": "^10.0.0-alpha.5"
70
70
  },
71
71
  "files": [
72
72
  "dist",