@nodefony/security 10.0.0-alpha.8 → 10.0.0-beta.1

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 (83) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/nodefony/command/security-token.js +4 -4
  5. package/dist/nodefony/command/security-user-add.js +2 -18
  6. package/dist/nodefony/command/security-user-delete.js +2 -2
  7. package/dist/nodefony/command/security-user-list.js +1 -1
  8. package/dist/nodefony/command/security-user-password.js +2 -2
  9. package/dist/nodefony/config/config.js +2 -2
  10. package/dist/nodefony/service/apiKeys.js +2 -2
  11. package/dist/nodefony/service/auditService.js +6 -5
  12. package/dist/nodefony/service/authFlow.js +2 -2
  13. package/dist/nodefony/service/authorization.js +3 -4
  14. package/dist/nodefony/service/firewall.js +21 -9
  15. package/dist/nodefony/service/oauth2.js +5 -3
  16. package/dist/nodefony/service/securityHeaders.js +1 -1
  17. package/dist/nodefony/service/tokenService.js +30 -20
  18. package/dist/nodefony/service/totp.js +5 -3
  19. package/dist/nodefony/service/webAuthn.js +41 -33
  20. package/dist/nodefony/service/webhooks.js +55 -44
  21. package/dist/nodefony/src/admin/SecurityAdminApi.js +2 -1
  22. package/dist/nodefony/src/admin/WebhookAdminApi.js +1 -0
  23. package/dist/nodefony/src/admin/userRevocationCascade.js +1 -1
  24. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -1
  25. package/dist/nodefony/src/audit/MemoryAuditStore.js +4 -8
  26. package/dist/nodefony/src/audit/auditBridge.js +9 -5
  27. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -3
  28. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +2 -2
  29. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +2 -2
  30. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +2 -2
  31. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +5 -3
  32. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +2 -2
  33. package/dist/nodefony/src/csp.js +4 -5
  34. package/dist/nodefony/src/oauth/providers/github.js +1 -1
  35. package/dist/nodefony/src/realtime/frameAuthorizer.js +6 -6
  36. package/dist/nodefony/src/token/JwtKeystore.js +7 -6
  37. package/dist/nodefony/src/token/MemoryTokenStore.js +19 -6
  38. package/dist/nodefony/src/token/jwtRuntime.js +13 -1
  39. package/dist/nodefony/src/token/tokenCriteria.js +1 -0
  40. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +15 -3
  41. package/dist/nodefony/src/totp/totpCrypto.js +9 -9
  42. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +24 -6
  43. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +18 -10
  44. package/dist/nodefony/src/webhook/WebhookDispatcher.js +4 -3
  45. package/dist/types/nodefony/command/security-user-add.d.ts +0 -16
  46. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +8 -4
  47. package/dist/types/nodefony/contracts/IFirewall.d.ts +2 -2
  48. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +5 -5
  49. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +4 -4
  50. package/dist/types/nodefony/contracts/ITokenStore.d.ts +3 -3
  51. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +4 -4
  52. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +1 -1
  53. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +1 -1
  54. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +1 -1
  55. package/dist/types/nodefony/service/firewall.d.ts +3 -2
  56. package/dist/types/nodefony/service/securityHeaders.d.ts +5 -5
  57. package/dist/types/nodefony/src/SecuredArea.d.ts +2 -2
  58. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +5 -3
  59. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +2 -2
  60. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +2 -2
  61. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +3 -3
  62. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +2 -2
  63. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +2 -2
  64. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +10 -0
  65. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +0 -9
  66. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +0 -8
  67. package/docs/api-keys.md +67 -65
  68. package/docs/audit.md +87 -82
  69. package/docs/authenticators.md +61 -49
  70. package/docs/authorization.md +21 -21
  71. package/docs/cors.md +30 -30
  72. package/docs/csrf.md +28 -28
  73. package/docs/external-jwt.md +21 -13
  74. package/docs/firewall.md +32 -32
  75. package/docs/headers.md +29 -29
  76. package/docs/index.md +3 -1
  77. package/docs/oauth2.md +61 -61
  78. package/docs/obtenir-un-jeton.md +2 -2
  79. package/docs/tokens.md +56 -56
  80. package/docs/totp.md +73 -72
  81. package/docs/webauthn.md +85 -85
  82. package/docs/webhooks.md +50 -47
  83. package/package.json +19 -16
package/docs/webhooks.md CHANGED
@@ -66,11 +66,11 @@ Le détail de chaque étape est plus bas, dans **Architecture interne**.
66
66
 
67
67
  Trois usages courants, tous branchés sur des événements que Nodefony émet déjà :
68
68
 
69
- | Ce que tu veux | Tu abonnes | Le tiers qui reçoit |
70
- | -------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------- |
71
- | Être prévenu quand quelqu'un s'acharne sur un compte | `login.failure` | un canal Slack, un SMS d'astreinte |
72
- | Garder une trace inviolable des accès, hors de l'application | `*` | un SIEM, un bucket d'archives |
73
- | Couper l'accès d'un salarié partout ailleurs quand sa session est révoquée | `token.revoked`, `session.destroyed` | ton annuaire, ton outil de tickets |
69
+ | Ce que tu veux | Tu abonnes | Le tiers qui reçoit |
70
+ | -------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------------- |
71
+ | Être prévenu quand quelqu'un s'acharne sur un compte | `login.failure` | un canal Slack, un SMS d'astreinte |
72
+ | Garder une trace inviolable des accès, hors de l'application | `*` | un SIEM, un bucket d'archives |
73
+ | Couper l'accès d'un salarié partout ailleurs quand sa session est révoquée | `session.revoked`, `token.reuse_detected` | ton annuaire, ton outil de tickets |
74
74
 
75
75
  Le premier, en entier — un serveur qui prévient une équipe quand un compte est attaqué :
76
76
 
@@ -146,7 +146,7 @@ propre serveur**.
146
146
  Trois partis pris, tous vérifiables dans le code :
147
147
 
148
148
  1. **La source est le journal d'audit, pas un bus métier.** `WebhookService.#attachDispatcher()`
149
- (`webhooks.ts:185`) s'abonne au service `auditService` et à lui seul. Si l'audit est absent, le
149
+ (`webhooks.ts:198`) s'abonne au service `auditService` et à lui seul. Si l'audit est absent, le
150
150
  dispatcher n'existe pas — le CRUD d'endpoints reste disponible, mais **rien ne part**. C'est une
151
151
  limite assumée : les webhooks notifient des **événements de sécurité**.
152
152
  2. **Le secret est chiffré, jamais haché.** Contrairement à une clé d'API (qu'on vérifie donc qu'on
@@ -160,11 +160,11 @@ Trois partis pris, tous vérifiables dans le code :
160
160
 
161
161
  ### 1. Activer les webhooks et poser la clé de chiffrement
162
162
 
163
- Les webhooks sont **actifs par défaut** (`enabled: true` dans le schéma Zod, `security/nodefony/config/config.ts:654`).
163
+ Les webhooks sont **actifs par défaut** (`enabled: true` dans le schéma Zod, `security/nodefony/config/config.ts:785`).
164
164
  La seule chose que tu dois vraiment fournir, c'est la **clé de chiffrement des secrets de signature** :
165
165
  sans elle, une clé éphémère est générée en dev (avec un WARNING), et en production les webhooks sont
166
166
  **désactivés** — un secret chiffré par une clé perdue au redémarrage serait illisible
167
- (`WebhookService.#resolveKey()`, `webhooks.ts:299`).
167
+ (`WebhookService.#resolveKey()`, `webhooks.ts:316`).
168
168
 
169
169
  ```bash
170
170
  # Génère les clés du module security et guide le câblage en 3 fichiers.
@@ -210,7 +210,7 @@ export default defineConfig<typeof env>((ctx) => ({
210
210
 
211
211
  Il n'y a **pas de déclaration d'endpoint en config** : un abonnement est une donnée, créée à
212
212
  l'exécution via le data plane admin `/nodefony/security/api/webhooks`, gardé
213
- `ROLE_NODEFONY_ADMIN` (`webhookAdminEndpoints()`, `WebhookAdminApi.ts:205`).
213
+ `ROLE_NODEFONY_ADMIN` (`webhookAdminEndpoints()`, `WebhookAdminApi.ts:219`).
214
214
 
215
215
  ```bash
216
216
  # Session admin (le BFF de login pose le cookie)
@@ -408,18 +408,18 @@ retries** : les tentatives intermédiaires ne sont pas tracées, seule l'issue p
408
408
  ## Quels événements partent en webhook ?
409
409
 
410
410
  Réponse honnête et vérifiable : **les actions du journal d'audit de sécurité, et rien d'autre**. Le
411
- dispatcher est branché sur `AuditService.subscribe()` (`auditService.ts:205`), appelé dans le
412
- fire-and-forget de `AuditService.record()` (`auditService.ts:180`). Aucun autre point d'émission
411
+ dispatcher est branché sur `AuditService.subscribe()` (`auditService.ts:212`), appelé dans le
412
+ fire-and-forget de `AuditService.record()` (`auditService.ts:187`). Aucun autre point d'émission
413
413
  n'existe dans le code.
414
414
 
415
- Les catégories d'audit disponibles (`AuditCategory`, `IAuditEvent.ts:16`) donnent la surface réelle :
415
+ Les catégories d'audit disponibles (`AuditCategory`, `IAuditEvent.ts:24`) donnent la surface réelle :
416
416
 
417
417
  | Catégorie | Ce qu'elle trace (exemples d'`action`) |
418
418
  | ---------- | -------------------------------------------------------------------------- |
419
419
  | `auth` | Login/logout, chaîne d'authentification (`login.success`, `login.failure`) |
420
420
  | `authz` | Accès accordé/refusé, voters, `@IsGranted` (`access.denied`) |
421
- | `token` | Jetons longue durée émis/révoqués — JWT refresh, PAT (`token.revoked`) |
422
- | `session` | Cycle de vie de session (`session.opened`) |
421
+ | `token` | Jetons longue durée et clés d'API (`token.issued`, `apikey.revoked`) |
422
+ | `session` | Cycle de vie de session (`logout`, `session.revoked`) |
423
423
  | `oauth` | Login social : authorize, callback, provisioning JIT |
424
424
  | `webauthn` | Passkeys : enregistrement, assertion |
425
425
  | `csrf` | Défense CSRF déclenchée |
@@ -428,6 +428,10 @@ Les catégories d'audit disponibles (`AuditCategory`, `IAuditEvent.ts:16`) donne
428
428
  | `webhook` | Vie des webhooks eux-mêmes (`webhook.created`, `webhook.disabled`) |
429
429
  | `config` | Mutation de config runtime depuis Studio |
430
430
 
431
+ > [!NOTE]
432
+ > Trois catégories sont **réservées** au contrat sans émetteur à ce jour : `oauth`, `csrf`, `cors`
433
+ > (`IAuditEvent.ts:16`). S'y abonner est accepté, mais ne livre jamais rien.
434
+
431
435
  ### La syntaxe d'abonnement
432
436
 
433
437
  Le champ `events` d'un endpoint accepte trois formes, résolues par `matchesSubscription()`
@@ -480,7 +484,7 @@ qui comptent :
480
484
  | `webhook-timestamp` | epoch **secondes** | fenêtre **anti-rejeu** |
481
485
  | `webhook-signature` | `v1,<base64(HMAC-SHA256)>` | preuve de l'émetteur |
482
486
 
483
- Le secret est un `whsec_<base64 de 256 bits>` généré par `generateSecret()` (`webhooks.ts:108`) ;
487
+ Le secret est un `whsec_<base64 de 256 bits>` généré par `generateSecret()` (`webhooks.ts:117`) ;
484
488
  `parseWebhookSecret()` (`webhookSignature.ts:22`) décode la partie base64 — le préfixe n'entre pas
485
489
  dans la clé HMAC, et un secret déjà sans préfixe est toléré.
486
490
 
@@ -510,7 +514,7 @@ curl -sk -b /tmp/jar -X POST \
510
514
  # {"endpoint":{…}, "secret":"whsec_NOUVEAU…"}
511
515
  ```
512
516
 
513
- `WebhookService.rotateSecret()` (`webhooks.ts:553`) régénère et rechiffre. Comportement à connaître
517
+ `WebhookService.rotateSecret()` (`webhooks.ts:560`) régénère et rechiffre. Comportement à connaître
514
518
  **avant** de cliquer :
515
519
 
516
520
  - l'ancien secret cesse d'être valide **immédiatement** — il n'y a pas de fenêtre de recouvrement
@@ -668,7 +672,7 @@ abandon. Chaque retry **repasse par la file bornée** (`#scheduleRetry()`,
668
672
 
669
673
  ### Auto-désactivation d'un endpoint mort
670
674
 
671
- Chaque issue finale passe par `WebhookService.markDelivery()` (`webhooks.ts:680`) : succès →
675
+ Chaque issue finale passe par `WebhookService.markDelivery()` (`webhooks.ts:682`) : succès →
672
676
  `failureCount = 0` ; échec → incrément. Au-delà de `autoDisableThreshold` (défaut **20**), l'endpoint
673
677
  est **désactivé** et un unique événement d'audit `webhook.disabled` est émis — **un par endpoint qui
674
678
  meurt**, jamais un par échec (le volume resterait ingérable). Mettre le seuil à `0` désactive
@@ -681,7 +685,7 @@ Le scénario vécu, du début à la fin :
681
685
  1. **Tentative 1** → `ECONNREFUSED`. Classé `retry` ; rien n'est encore écrit en base.
682
686
  2. **Tentatives 2 à 6** sur ~4 min. Toujours rien de persisté (seule l'issue finale l'est).
683
687
  3. **Abandon.** `markDelivery` écrit `lastDeliveryStatus: null`, `lastDeliveryError`, et incrémente
684
- `failureCount`. Une trace part dans l'historique RAM (`#recordDelivery()`, `webhooks.ts:605`).
688
+ `failureCount`. Une trace part dans l'historique RAM (`#recordDelivery()`, `webhooks.ts:612`).
685
689
  4. **L'événement est PERDU.** Il n'y a pas de file persistée : un webhook est **best-effort**. Rien
686
690
  ne sera rejoué quand le destinataire reviendra.
687
691
  5. **Après 20 échecs consécutifs**, l'endpoint passe `enabled: false` et cesse de consommer des
@@ -694,14 +698,14 @@ Le scénario vécu, du début à la fin :
694
698
 
695
699
  ### Arrêt propre
696
700
 
697
- `WebhookService.#shutdown()` (`webhooks.ts:221`) se désabonne de l'audit puis appelle
701
+ `WebhookService.#shutdown()` (`webhooks.ts:234`) se désabonne de l'audit puis appelle
698
702
  `WebhookDispatcher.shutdown()` (`WebhookDispatcher.ts:280`) : admission stoppée, **tous les timers de
699
703
  retry annulés**, file relâchée. Aucun timer orphelin ne retient le process — et les timers de retry
700
- sont `unref()` (`webhooks.ts:209`), donc ils n'empêchent jamais Node de sortir.
704
+ sont `unref()` (`webhooks.ts:222`), donc ils n'empêchent jamais Node de sortir.
701
705
 
702
706
  ## ⚙️ Configuration
703
707
 
704
- Section `webhooks` du schéma Zod (`webhooksSchema`, `security/nodefony/config/config.ts:777`), lue via
708
+ Section `webhooks` du schéma Zod (`webhooksSchema`, `security/nodefony/config/config.ts:783`), lue via
705
709
  `use("@nodefony/security", { webhooks: … })`.
706
710
 
707
711
  | Option | Type | Défaut | Effet |
@@ -721,7 +725,7 @@ Section `webhooks` du schéma Zod (`webhooksSchema`, `security/nodefony/config/c
721
725
 
722
726
  > [!NOTE]
723
727
  > `timestampToleranceS` est **transporté** dans la politique de livraison
724
- > (`getDeliveryPolicy()`, `webhooks.ts:660`) mais l'émetteur ne l'applique jamais : la fenêtre
728
+ > (`getDeliveryPolicy()`, `webhooks.ts:663`) mais l'émetteur ne l'applique jamais : la fenêtre
725
729
  > anti-rejeu est par nature un contrôle du **récepteur**. Traite cette valeur comme la tolérance que
726
730
  > tu documentes à tes destinataires — c'est celle du récepteur qui protège.
727
731
 
@@ -756,7 +760,7 @@ par dialecte via le `colKit`. Côté documentaire, le schéma force `_id: String
756
760
  (`webhookEndpointSchema`, `mongoose/nodefony/entity/webhookEndpointEntity.ts:23`).
757
761
 
758
762
  Ce qui **n'est pas** persisté : l'historique des livraisons. Il vit en RAM, borné à 20 entrées par
759
- endpoint (`MAX_DELIVERIES_PER_ENDPOINT`, `webhooks.ts:54`), corps de requête tronqué à 8 Ko, corps
763
+ endpoint (`MAX_DELIVERIES_PER_ENDPOINT`, `webhooks.ts:63`), corps de requête tronqué à 8 Ko, corps
760
764
  de réponse à 2 Ko (`RESPONSE_BODY_CAP`, `webhookDelivery.ts:21`) — et il est **par pod**. C'est de
761
765
  l'observabilité éphémère de mise au point, pas un journal d'audit.
762
766
 
@@ -770,9 +774,9 @@ l'observabilité éphémère de mise au point, pas un journal d'audit.
770
774
  | `redis` | **volontairement absent** | — | — | un registre n'est pas un cache |
771
775
 
772
776
  Redis n'est pas une omission : un endpoint est de la **configuration durable**, sa place n'est pas
773
- dans un magasin volatil (`IWebhookStore.ts:29`).
777
+ dans un magasin volatil (`IWebhookStore.ts:43`).
774
778
 
775
- La résolution est explicite et **annoncée**. `WebhookService.#resolveStore()` (`webhooks.ts:231`)
779
+ La résolution est explicite et **annoncée**. `WebhookService.#resolveStore()` (`webhooks.ts:244`)
776
780
  privilégie un adapter déjà posé au container, puis résout `auto` d'après l'infra déclarée, et
777
781
  **enregistre sa décision** (visible dans Studio). Deux garde-fous :
778
782
 
@@ -782,7 +786,7 @@ privilégie un adapter déjà posé au container, puis résout `auto` d'après l
782
786
 
783
787
  ### Pagination du registre
784
788
 
785
- `WebhookService.listPage()` (`webhooks.ts:126`) délègue au store — la console n'a **jamais** tout le
789
+ `WebhookService.listPage()` (`webhooks.ts:475`) délègue au store — la console n'a **jamais** tout le
786
790
  registre en RAM. Ce contrat est vérifié par un **banc unique** rejoué sur tous les backends
787
791
  (`webhookPaginationContract.ts`) : mêmes 12 endpoints de seed, mêmes assertions.
788
792
 
@@ -800,14 +804,14 @@ registre en RAM. Ce contrat est vérifié par un **banc unique** rejoué sur tou
800
804
  consécutif courant — « qu'est-ce qui casse ? », `IWebhookStore.ts:35`), `q` (sous-chaîne
801
805
  insensible à la casse sur `url` **ou** `description`).
802
806
  - Mode unique **offset** : tous les backends d'endpoints savent le faire, aucune capacité n'est donc
803
- à déclarer (`MemoryWebhookStore.listPage()`, `MemoryWebhookStore.ts:69` ;
804
- `DrizzleWebhookStore.ts:159` ; `MongooseWebhookStore.ts:188`).
807
+ à déclarer (`MemoryWebhookStore.listPage()`, `MemoryWebhookStore.ts:90` ;
808
+ `DrizzleWebhookStore.ts:185` ; `MongooseWebhookStore.ts:227`).
805
809
  - **Les compteurs suivent la recherche.** `GET webhooks/stats` déclare `search`
806
810
  (`WebhookAdminApi.ts:307`) et descend le même `q` jusqu'au store : un terme sans correspondance
807
811
  vide les cartes autant que le tableau. Sans cela, la console afficherait « 12 endpoints » au-dessus
808
812
  d'une liste filtrée à 2 — un chiffre qui ne répond plus à la question posée à l'écran.
809
813
 
810
- `IWebhookStore.listAll()` (`IWebhookStore.ts:57`) existe toujours, mais il est **réservé au snapshot
814
+ `IWebhookStore.listAll()` (`IWebhookStore.ts:72`) existe toujours, mais il est **réservé au snapshot
811
815
  du dispatcher** : celui-ci doit connaître tous les abonnements pour ne rater aucune livraison. C'est
812
816
  un cold-path (boot + après écriture CRUD), jamais un chemin d'affichage.
813
817
 
@@ -820,17 +824,17 @@ mention.
820
824
 
821
825
  | Méthode | Rôle | Ancrage |
822
826
  | ----------------------------- | ------------------------------------------------------------------ | ----------------- |
823
- | `register(input)` | Crée un endpoint (SSRF validé) → endpoint **+ secret en clair** | `webhooks.ts:434` |
824
- | `listPage(query)` | Page d'endpoints (vue publique, sans secret) | `webhooks.ts:468` |
825
- | `countEndpoints(query)` | `COUNT` natif ; `-1` si le backend ne sait pas compter | `webhooks.ts:456` |
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:458` |
828
- | `setEnabled(id, bool)` | Révocation douce | `webhooks.ts:542` |
829
- | `rotateSecret(id)` | Nouveau secret ; l'ancien meurt immédiatement | `webhooks.ts:553` |
830
- | `revealSecret(id)` | Secret en clair (action sensible, à auditer par l'appelant) | `webhooks.ts:572` |
831
- | `delete(id)` | Supprime ; `false` si absent | `webhooks.ts:580` |
832
- | `listDeliveries(id)` _(sync)_ | Historique RAM des dernières livraisons | `webhooks.ts:595` |
833
- | `isReady()` _(sync)_ | Activé **et** store **et** clé résolus | `webhooks.ts:407` |
827
+ | `register(input)` | Crée un endpoint (SSRF validé) → endpoint **+ secret en clair** | `webhooks.ts:441` |
828
+ | `listPage(query)` | Page d'endpoints (vue publique, sans secret) | `webhooks.ts:475` |
829
+ | `countEndpoints(query)` | `COUNT` natif ; `-1` si le backend ne sait pas compter | `webhooks.ts:489` |
830
+ | `getEndpoint(id)` | Un endpoint (vue publique) ou `null` | `webhooks.ts:517` |
831
+ | `update(id, patch)` | `url`/`events`/`enabled`/`description`/`metadata` ; URL re-validée | `webhooks.ts:528` |
832
+ | `setEnabled(id, bool)` | Révocation douce | `webhooks.ts:549` |
833
+ | `rotateSecret(id)` | Nouveau secret ; l'ancien meurt immédiatement | `webhooks.ts:560` |
834
+ | `revealSecret(id)` | Secret en clair (action sensible, à auditer par l'appelant) | `webhooks.ts:579` |
835
+ | `delete(id)` | Supprime ; `false` si absent | `webhooks.ts:587` |
836
+ | `listDeliveries(id)` _(sync)_ | Historique RAM des dernières livraisons | `webhooks.ts:602` |
837
+ | `isReady()` _(sync)_ | Activé **et** store **et** clé résolus | `webhooks.ts:414` |
834
838
 
835
839
  Types et briques réutilisables exportés par `@nodefony/security` : `IWebhookEndpoint`,
836
840
  `WebhookEndpointSummary`, `IWebhookStore`, `IWebhookListQuery`, `MemoryWebhookStore`,
@@ -839,13 +843,14 @@ Types et briques réutilisables exportés par `@nodefony/security` : `IWebhookEn
839
843
 
840
844
  ### Le data plane d'administration
841
845
 
842
- Huit endpoints sous `/nodefony/security/api/webhooks`, tous `ROLE_NODEFONY_ADMIN`, composés dans le
846
+ Neuf endpoints sous `/nodefony/security/api/webhooks`, tous `ROLE_NODEFONY_ADMIN`, composés dans le
843
847
  producteur `security` — ils héritent gratuitement du RBAC fail-closed du broker, de l'audit et de la
844
848
  porte d'idempotence sur les mutations.
845
849
 
846
850
  | Méthode + chemin | Rôle | Audit |
847
851
  | ------------------------------ | --------------------------------------------------------- | ------------------ |
848
852
  | `GET webhooks` | Page d'endpoints + driver du store. **Jamais de secret.** | — |
853
+ | `GET webhooks/stats` | Compteurs sur la collection entière, mêmes filtres | — |
849
854
  | `POST webhooks` | Crée ; secret renvoyé **une seule fois** ; `422` si SSRF | `webhook.created` |
850
855
  | `GET webhooks/{id}` | Un endpoint (vue publique), `404` sinon | — |
851
856
  | `GET webhooks/{id}/deliveries` | Historique RAM des livraisons de cet endpoint | — |
@@ -863,7 +868,7 @@ Deux détails de conception qui se voient à l'usage :
863
868
  honnête (`enabled: false`) et une liste vide, plutôt qu'une erreur. Les **mutations**, elles,
864
869
  rendent bien `503` si le service n'est pas prêt.
865
870
  - **Le listing est borné** : `limit` par défaut 50, plafond dur **200**
866
- (`parseWebhookListQuery()`, `WebhookAdminApi.ts:147`) — un client ne peut pas demander « tout ».
871
+ (`parseWebhookListQuery()`, `WebhookAdminApi.ts:161`) — un client ne peut pas demander « tout ».
867
872
 
868
873
  ## 🧩 Extension — brancher son propre registre
869
874
 
@@ -910,7 +915,7 @@ qu'il y en a.
910
915
  | File d'attente | `maxQueue` (1000) puis **abandon** + log | `WebhookDispatcher.ts:149` |
911
916
  | Sockets / FD simultanés | `maxConcurrent` (8) | `WebhookDispatcher.#pump()` (`WebhookDispatcher.ts:173`) |
912
917
  | Durée d'une tentative | 10 s puis `req.destroy()` | `webhookDelivery.ts:136` |
913
- | Historique par endpoint | 20 entrées, corps requête 8 Ko, réponse 2 Ko | `webhooks.ts:54` |
918
+ | Historique par endpoint | 20 entrées, corps requête 8 Ko, réponse 2 Ko | `webhooks.ts:63` · `webhookDelivery.ts:21` |
914
919
  | Allocations paresseuses | file, `Set` de timers, historique : `null` tant qu'inutilisés | `WebhookDispatcher.ts:114` |
915
920
 
916
921
  La preuve n'est pas déclarative : un banc d'attaque envoie **5000 événements vers un endpoint mort**
@@ -933,7 +938,7 @@ exactement le data plane décrit plus haut (`WEBHOOKS_ENDPOINT`, `webhooksModel.
933
938
  - **panneau des livraisons récentes** — ce qui a été envoyé et ce que le destinataire a répondu
934
939
  (`DeliveriesPanel.tsx`) ;
935
940
  - **badge « où on écrit »** : `memory` ou `orm`, dérivé du nom de classe réel du store
936
- (`webhookStoreDriver()`, `WebhookAdminApi.ts:101`) — un store tiers inconnu affiche `null` plutôt
941
+ (`webhookStoreDriver()`, `WebhookAdminApi.ts:113`) — un store tiers inconnu affiche `null` plutôt
937
942
  qu'un driver inventé.
938
943
 
939
944
  En développement, le module `test` fournit un **récepteur local** à demeure — le remplaçant
@@ -947,6 +952,7 @@ secret, et sait **simuler des pannes** pour observer retries et auto-désactivat
947
952
  | `POST /nodefony/test/webhooks/sink/status/{code}` | Répond le code demandé → simule un récepteur en erreur |
948
953
  | `POST /nodefony/test/webhooks/sink/slow?ms=…` | Répond lentement → provoque le timeout de livraison |
949
954
  | `GET /nodefony/test/webhooks/received` | Inspecte les livraisons reçues |
955
+ | `DELETE /nodefony/test/webhooks/received` | Vide le journal des livraisons reçues |
950
956
 
951
957
  ## ⚠️ Pièges (symptôme → cause → correction)
952
958
 
@@ -1011,6 +1017,3 @@ Couverture : `npm run coverage` dans `@nodefony/security`. Revue de sécurité d
1011
1017
  - La **source des événements** livrés → [audit](audit.md) · Le pare-feu qui les produit → [firewall](firewall.md)
1012
1018
  - Secrets et jetons du module, mêmes principes de chiffrement au repos → [tokens](tokens.md)
1013
1019
  - Vocabulaire transverse de la sécurité → [lexique](lexique.md)
1014
- </content>
1015
-
1016
- </invoke>
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@nodefony/security",
3
- "version": "10.0.0-alpha.8",
3
+ "version": "10.0.0-beta.1",
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",
7
7
  "type": "module",
8
+ "nodefony": {
9
+ "consumesStores": true
10
+ },
8
11
  "types": "./dist/types/index.d.ts",
9
12
  "exports": {
10
13
  ".": {
14
+ "nodefony-source": "./index.ts",
11
15
  "types": "./dist/types/index.d.ts",
12
16
  "import": "./dist/index.js",
13
17
  "default": "./dist/index.js"
@@ -15,9 +19,9 @@
15
19
  },
16
20
  "scripts": {
17
21
  "start": "node dist/index.js",
18
- "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
22
+ "build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
19
23
  "dev": "rolldown -c rolldown.config.ts --watch",
20
- "clean": "rimraf dist",
24
+ "clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
21
25
  "test": "vitest run",
22
26
  "coverage": "vitest run --coverage",
23
27
  "typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
@@ -44,29 +48,28 @@
44
48
  "directory": "src/packages/@nodefony/security"
45
49
  },
46
50
  "dependencies": {
47
- "@simplewebauthn/server": "14.0.2",
51
+ "@simplewebauthn/server": "14.0.3",
48
52
  "jose": "6.2.12",
49
53
  "tslib": "2.8.1",
50
54
  "zod": "^4.6.5"
51
55
  },
52
56
  "devDependencies": {
53
- "@nodefony/framework": "^10.0.0-alpha.8",
54
- "@nodefony/http": "^10.0.0-alpha.8",
55
- "@nodefony/user": "^10.0.0-alpha.8",
56
- "@types/node": "26.6.2",
57
- "@vitest/coverage-v8": "5.0.1",
58
- "nodefony": "^10.0.0-alpha.8",
59
- "rimraf": "6.1.3",
60
- "vitest": "5.0.1"
57
+ "@nodefony/framework": "^10.0.0-beta.1",
58
+ "@nodefony/http": "^10.0.0-beta.1",
59
+ "@nodefony/user": "^10.0.0-beta.1",
60
+ "@types/node": "26.6.3",
61
+ "@vitest/coverage-v8": "5.0.2",
62
+ "nodefony": "^10.0.0-beta.1",
63
+ "vitest": "5.0.2"
61
64
  },
62
65
  "license": "Apache-2.0",
63
66
  "readmeFilename": "README.md",
64
67
  "contributors": [],
65
68
  "peerDependencies": {
66
- "@nodefony/framework": "^10.0.0-alpha.8",
67
- "@nodefony/http": "^10.0.0-alpha.8",
68
- "@nodefony/user": "^10.0.0-alpha.8",
69
- "nodefony": "^10.0.0-alpha.8"
69
+ "@nodefony/framework": "^10.0.0-beta.1",
70
+ "@nodefony/http": "^10.0.0-beta.1",
71
+ "@nodefony/user": "^10.0.0-beta.1",
72
+ "nodefony": "^10.0.0-beta.1"
70
73
  },
71
74
  "files": [
72
75
  "dist",