@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.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 (165) 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 +2 -3
  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/nodefony/testing/index.js +108 -0
  125. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
  126. package/dist/types/nodefony/config/config.d.ts +1 -0
  127. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  128. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  129. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  130. package/dist/types/nodefony/service/oauth2.d.ts +20 -0
  131. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  132. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
  133. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
  134. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  135. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
  136. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  137. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  138. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  139. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  140. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
  141. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  142. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  143. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  144. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  145. package/dist/types/nodefony/testing/index.d.ts +51 -0
  146. package/docs/api-keys.md +181 -174
  147. package/docs/audit.md +140 -140
  148. package/docs/authenticators.md +101 -101
  149. package/docs/authorization.md +116 -69
  150. package/docs/cors.md +64 -64
  151. package/docs/csrf.md +74 -71
  152. package/docs/external-jwt.md +22 -12
  153. package/docs/firewall.md +85 -82
  154. package/docs/headers.md +81 -81
  155. package/docs/index.md +4 -4
  156. package/docs/keycloak.md +95 -78
  157. package/docs/login-page.md +23 -23
  158. package/docs/oauth2.md +157 -146
  159. package/docs/obtenir-un-jeton.md +116 -26
  160. package/docs/tokens.md +128 -122
  161. package/docs/totp.md +136 -136
  162. package/docs/webauthn.md +153 -153
  163. package/docs/webhooks.md +136 -124
  164. package/package.json +19 -13
  165. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/firewall.md CHANGED
@@ -52,10 +52,10 @@ flowchart TD
52
52
  AU -->|succès| OK["user + token dans l'ALS → contrôleur"]
53
53
  ```
54
54
 
55
- `Firewall.isSecure()` (`firewall.ts:792`) rattache la requête à une **zone** via
56
- `Firewall.matchPath()` (`firewall.ts:783`) ; `Firewall.handleSecurity()` (`firewall.ts:825`) décide.
55
+ `Firewall.isSecure()` (`firewall.ts#Firewall.isSecure`) rattache la requête à une **zone** via
56
+ `Firewall.matchPath()` (`firewall.ts#Firewall.matchPath`) ; `Firewall.handleSecurity()` (`firewall.ts#Firewall.handleSecurity`) décide.
57
57
  Les zones sont triées par **spécificité** dans `#build()` — `list.sort` par longueur de motif :
58
- le plus long gagne, pas le premier déclaré (`firewall.ts:258`).
58
+ le plus long gagne, pas le premier déclaré (`firewall.ts#Firewall.#build`).
59
59
 
60
60
  ## 📖 Lexique
61
61
 
@@ -74,31 +74,34 @@ le plus long gagne, pas le premier déclaré (`firewall.ts:258`).
74
74
  ### Dans une app `nodefony create app`, le firewall est DÉJÀ actif
75
75
 
76
76
  Le scaffold déclare trois zones (`main`, `secure`, `machine`) dans `nodefony/config/security.ts`, importé par `nodefony.config.ts` ; en voici deux — c'est la forme canonique (un **objet par
77
- nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts:1443-1444`) :
77
+ nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts#securityConfigSchema`) :
78
78
 
79
79
  ```typescript
80
- // nodefony/config/security.ts (extrait généré par `nodefony create app`)
81
- use("@nodefony/security", {
82
- areas: {
83
- // Zone de TES routes : `session` PUIS `anonymous` → identifié si cookie,
84
- // sinon visiteur accepté. Hors zone, l'identité n'est JAMAIS résolue.
85
- main: {
86
- pattern: "^/api",
87
- authenticators: ["session", "anonymous"],
80
+ // nodefony/config/security.ts
81
+ import type { ISecurityConfigInput } from "@nodefony/security";
82
+
83
+ export const securityConfig = () =>
84
+ ({
85
+ areas: {
86
+ // Zone de TES routes : `session` PUIS `anonymous` → identifié si cookie,
87
+ // sinon visiteur accepté. Hors zone, l'identité n'est JAMAIS résolue.
88
+ main: {
89
+ pattern: "^/api",
90
+ authenticators: ["session", "anonymous"],
91
+ },
92
+ // Zone PROTÉGÉE — pattern PLUS SPÉCIFIQUE que ^/api : le firewall trie
93
+ // par longueur → /api/secure/* tombe ICI. Pas d'`anonymous` : sans
94
+ // session → 401 AVANT ton controller (Zero Trust).
95
+ secure: {
96
+ pattern: "^/api/secure",
97
+ authenticators: ["session"],
98
+ },
88
99
  },
89
- // Zone PROTÉGÉE — pattern PLUS SPÉCIFIQUE que ^/api : le firewall trie
90
- // par longueur → /api/secure/* tombe ICI. Pas d'`anonymous` : sans
91
- // session → 401 AVANT ton controller (Zero Trust).
92
- secure: {
93
- pattern: "^/api/secure",
94
- authenticators: ["session"],
100
+ roleHierarchy: {
101
+ ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
102
+ ROLE_ADMIN: ["ROLE_USER"],
95
103
  },
96
- },
97
- roleHierarchy: {
98
- ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
99
- ROLE_ADMIN: ["ROLE_USER"],
100
- },
101
- });
104
+ }) satisfies ISecurityConfigInput;
102
105
  ```
103
106
 
104
107
  > [!IMPORTANT]
@@ -236,9 +239,9 @@ Point commun de sécurité : **message d'échec uniforme** (`"Invalid token"` /
236
239
  Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
237
240
 
238
241
  - **N'ouvre jamais la session lui-même** : il exige une session reprise portant un user
239
- (`supports()`, `SessionAuthenticator.ts:75`). C'est `AuthFlow.login()` (BFF) qui ouvre et
242
+ (`supports()`, `SessionAuthenticator.ts#SessionAuthenticator.supports`). C'est `AuthFlow.login()` (BFF) qui ouvre et
240
243
  régénère l'ID (anti-fixation).
241
- - **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts:96`) → rôles frais,
244
+ - **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts#SessionAuthenticator.authenticate`) → rôles frais,
242
245
  révocation et verrouillage effectifs immédiatement.
243
246
  - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login
244
247
  (pas de popup Basic).
@@ -246,11 +249,11 @@ Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
246
249
  ### `userpassword` — HTTP Basic, avec throttle NIST
247
250
 
248
251
  Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617, split au **premier**
249
- `:` (`UserPasswordAuthenticator.ts:74`).
252
+ `:` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`).
250
253
 
251
254
  - **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
252
255
  leurre anti-timing, re-hash. L'authenticator ne voit que le verdict.
253
- - **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts:101`) :
256
+ - **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) :
254
257
  un identifiant bloqué ne coûte **aucun hash argon2** — protège d'un DoS par hachage.
255
258
  Échec → backoff ; `ThrottledError` → **429 + `Retry-After`**.
256
259
  - Challenge : `Basic realm="nodefony"`.
@@ -259,23 +262,23 @@ Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617,
259
262
 
260
263
  ### `jwt` — Bearer JWT signé, durci RFC 8725
261
264
 
262
- Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts:21`).
265
+ Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts#COMPACT_JWS`).
263
266
  Réservé API service↔service / agents (le web reste sur la session). Access token **EdDSA** signé par
264
267
  le keystore du serveur. Défenses **dures**, prouvées en test (RFC 8725 JWT BCP), toutes dans
265
268
  `JwtAuthenticator.authenticate()` :
266
269
 
267
270
  - **allowlist d'algorithmes** `["EdDSA"]` — l'algo n'est **jamais** choisi d'après l'en-tête du
268
- token ; `alg=none` rejeté (`JwtAuthenticator.ts:120`).
271
+ token ; `alg=none` rejeté (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
269
272
  - **clé par `kid` depuis le JWKS LOCAL** (`createLocalJWKSet`) — jamais `jku`/`jwk` de l'en-tête
270
- (anti-injection de clé / SSRF, `JwtAuthenticator.ts:174`).
273
+ (anti-injection de clé / SSRF, `JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`).
271
274
  - **`aud` + `iss` obligatoires** + `typ:"at+jwt"` (un refresh présenté comme access est rejeté) +
272
- exp/nbf (`JwtAuthenticator.ts:119-124`).
275
+ exp/nbf (`JwtAuthenticator.ts#JwtAuthenticator.verifyForResource`).
273
276
  - **révocation** malgré l'auto-portage : denylist `jti` + `invalidBefore` par sujet
274
- (`JwtAuthenticator.ts:138-148`).
277
+ (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
275
278
  - **sujet revérifié** à réception (`loadUserByIdentifier(sub)`) : compte disparu/inactif/verrouillé
276
- = rejet (`JwtAuthenticator.ts:190-201`).
279
+ = rejet (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
277
280
 
278
- Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:178-188`).
281
+ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
279
282
 
280
283
  > [!WARNING]
281
284
  > Un JWT est **auto-porté** : sans état serveur il n'est **pas** révocable. C'est la denylist
@@ -284,26 +287,26 @@ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:178-188`).
284
287
 
285
288
  ### `apikey` — clé d'API opaque (PAT), révocable
286
289
 
287
- Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts:75-79`). Contrairement au JWT,
290
+ Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.supports`). Contrairement au JWT,
288
291
  c'est un **bearer opaque** : sa vérité vit côté serveur (`ITokenStore`) → **révocable immédiatement**.
289
292
 
290
293
  Défenses de `ApiKeyAuthenticator.authenticate()` :
291
294
 
292
295
  - **forme + CRC validés AVANT tout accès au store** — anti-DoS (`parseApiKey()`,
293
- `ApiKeyAuthenticator.ts:103`) ;
294
- - lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts:109`) ;
296
+ `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
297
+ - lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
295
298
  - révocation (`revokedAt`), expiration (`expiresAt`), **ban en masse** du porteur
296
- (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:111-124`) ;
297
- - **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts:127`) ;
298
- - `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts:136-143`).
299
+ (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
300
+ - **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
301
+ - `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
299
302
 
300
- Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:147-149`). `jwt` et `apikey` **cohabitent** dans
303
+ Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`). `jwt` et `apikey` **cohabitent** dans
301
304
  une zone : ils se discriminent par la forme (JWT = `a.b.c`, PAT = `prefix_…`).
302
305
 
303
306
  ### `anonymous` — accepter l'anonymat, explicitement
304
307
 
305
308
  Le **seul** authenticator qui produit un token non authentifié **sans** déclencher le Zero Trust
306
- (`AnonymousAuthenticator.ts:6-18`). À lister **volontairement** : `["jwt", "anonymous"]` en mode
309
+ (`AnonymousAuthenticator.ts#AnonymousAuthenticator`). À lister **volontairement** : `["jwt", "anonymous"]` en mode
307
310
  `first` = « identifié si preuve présente, sinon **visiteur anonyme accepté** ». En mode `all`, utile
308
311
  en dernier : « le canal doit être prouvé (ex. mTLS), l'identité utilisateur est optionnelle ». Coût
309
312
  nul : `supports()` accepte tout, le token porte le singleton gelé `anonymousUser` (0 allocation).
@@ -313,14 +316,14 @@ Sans lui, zone protégée + aucune preuve = 401.
313
316
 
314
317
  Il promeut en jeton realtime **toute** identité que le firewall a résolue — session BFF comme jeton
315
318
  porteur (JWT, clé d'API). **Enregistré automatiquement** par `Firewall.#wireRealtime()` au
316
- handshake des zones protégées `realtime` (`firewall.ts:302`).
319
+ handshake des zones protégées `realtime` (`firewall.ts#Firewall.#wireRealtime`).
317
320
 
318
321
  - **Perf : il ne relit pas la base.** Handshake et frames tournent dans la même bulle ALS —
319
322
  l'identité déjà posée est réutilisée, 2 lectures base économisées par connexion
320
- (`FirewallRealtimeAuthenticator.ts:24-30`).
323
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
321
324
  - **Asymétrie HTTP↔WS assumée** : le jeton est **figé au handshake** (les frames lisent un cache
322
325
  O(1)) → une révocation prend effet **en une fenêtre** (tick du hub, et devant chaque
323
- `api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts:51-55`). C'est l'état
326
+ `api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`). C'est l'état
324
327
  de l'art (Socket.IO et Phoenix figent aussi).
325
328
  - **Filet** : un revalidator re-lit la session avant chaque action data plane ; fail-closed →
326
329
  fermeture 4001.
@@ -353,7 +356,7 @@ laisser chercher pourquoi `context.session` est nul.
353
356
  ## ⚙️ Ordre et modes (`mode: "first"` vs `"all"`)
354
357
 
355
358
  La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
356
- (`firewall.ts:1236`). Le `mode` dit comment la parcourir. Trois situations concrètes :
359
+ (`firewall.ts#Firewall.#authenticate`). Le `mode` dit comment la parcourir. Trois situations concrètes :
357
360
 
358
361
  ### Situation 1 — humains ET machines sur la même API (`first`, le mode courant)
359
362
 
@@ -375,7 +378,7 @@ Ce qui se passe, requête par requête :
375
378
  | --- | --- | --- |
376
379
  | le cookie de session | `session` | identifié, `apikey` jamais consulté |
377
380
  | `Authorization: Bearer nf_…` | `apikey` | identifié (session ne matche pas, on passe) |
378
- | une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts:1236`) |
381
+ | une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts#Firewall.#authenticate`) |
379
382
  | rien | aucun | **401** (Zero Trust) |
380
383
 
381
384
  ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
@@ -406,11 +409,11 @@ danger: {
406
409
 
407
410
  Le client doit présenter **les deux preuves** dans la même requête (cookie + `Authorization:
408
411
  Basic …`). Une seule manque → 401. Le **dernier** token de la chaîne porte l'identité
409
- (`firewall.ts:1311`) — ici la preuve mot de passe, la plus fraîche.
412
+ (`firewall.ts#Firewall.#authenticate`) — ici la preuve mot de passe, la plus fraîche.
410
413
 
411
414
  > [!TIP]
412
415
  > Un nom d'authenticator inconnu en config **fait échouer le boot** —
413
- > `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts:468`) : jamais de zone
416
+ > `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts#Firewall.#instantiateAuthenticators`) : jamais de zone
414
417
  > « protégée » silencieusement ouverte à cause d'une faute de frappe.
415
418
 
416
419
  ## 🧑‍⚖️ Autorisation — rôles, scopes, voters (« as-tu le droit ? »)
@@ -432,30 +435,30 @@ du contrôleur** (seam Resolver) — une action protégée ne s'exécute jamais
432
435
 
433
436
  ### Le jury et sa stratégie
434
437
 
435
- `Authorization.decide(token, attribut, subject?)` (`service/authorization.ts:70`) applique une
438
+ `Authorization.decide(token, attribut, subject?)` (`service/authorization.ts#Authorization.decide`) applique une
436
439
  stratégie **affirmative + DENY veto**, fermée par défaut (**Zero Trust**) :
437
440
 
438
441
  - un seul **`DENY`** bloque (veto, court-circuit — inutile de finir le jury,
439
- `authorization.ts:94-97`) ;
442
+ `authorization.ts#Authorization.decide`) ;
440
443
  - sinon un **`GRANT`** suffit ;
441
444
  - **silence total** (tous `ABSTAIN`, ou aucun voter compétent) → **`DENY`**
442
- (`authorization.ts:100-108`) ;
445
+ (`authorization.ts#Authorization`) ;
443
446
  - un voter qui **throw** → **`DENY`** + log ERROR (fail-closed : jamais 500, jamais octroi,
444
- `authorization.ts:85-93`).
447
+ `authorization.ts#Authorization.decide`).
445
448
 
446
- Tout refus est audité (WARNING + `recordAudit`, `authorization.ts:113-142`) ; les octrois restent
449
+ Tout refus est audité (WARNING + `recordAudit`, `authorization.ts#Authorization`) ; les octrois restent
447
450
  muets (volume, pas un signal). Les voters sont instanciés **une fois au boot** via le registre
448
- (aucun nom en dur, `authorization.ts:55-64`).
451
+ (aucun nom en dur, `authorization.ts#Authorization`).
449
452
 
450
453
  ### Les voters intégrés — deux axes
451
454
 
452
455
  - **RoleVoter** (`role`, attributs `ROLE_*`) — `GRANT` si l'utilisateur a le rôle, **hiérarchie
453
- résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts:25-39`).
456
+ résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts#RoleVoter.vote`).
454
457
  Constat : l'absence d'un rôle ne doit pas opposer son veto aux autres axes — c'est le
455
458
  **default-DENY du jury** qui ferme la porte, pas ce voter. C'est ce qui rend une clause OR
456
459
  (`@IsGranted(["A","B"])`) possible.
457
460
  - **ScopeVoter** (`scope`, attributs `api:action`) — un scope **ne bride jamais un humain**
458
- (`ScopeVoter.ts:17-62`) :
461
+ (`ScopeVoter.ts`) :
459
462
  - jeton humain (`session`/`userpassword`/`anonymous`) → `GRANT` no-op : l'autorisation d'un
460
463
  humain passe par ses **rôles** ;
461
464
  - jeton **machine délégué** (`apikey`/`jwt`/`oauth2`) → `GRANT` si le scope exact est présent,
@@ -519,8 +522,8 @@ destinataire.
519
522
 
520
523
  `RoleHierarchyWalker` (`src/RoleHierarchyWalker.ts`) : `ROLE_ADMIN` hérite `ROLE_USER`, etc.
521
524
  **Aplatissement précalculé au boot** → `hasRole()` est O(1) sur le hot path
522
- (`RoleHierarchyWalker.ts:23-30`), et les **cycles sont détectés au boot** (throw avec le chemin
523
- complet, pas de fail-silent, `RoleHierarchyWalker.ts:69-95`). La hiérarchie est posée au container
525
+ (`RoleHierarchyWalker.ts#RoleHierarchyWalker.hasRole`), et les **cycles sont détectés au boot** (throw avec le chemin
526
+ complet, pas de fail-silent, `RoleHierarchyWalker.ts#RoleHierarchyWalker.#detectCycles`). La hiérarchie est posée au container
524
527
  par le firewall au boot ; le `RoleVoter` la lit en lazy.
525
528
 
526
529
  ### Voters métier (le vrai pouvoir applicatif)
@@ -539,41 +542,41 @@ registerVoterFactory(
539
542
  ```
540
543
 
541
544
  Le voter est **découvert automatiquement** par le service `authorization` — aucun changement dans le
542
- cœur (`registerVoterFactory()`, `voterRegistry.ts:40`). Pourquoi un registre et pas un scan DI des
545
+ cœur (`registerVoterFactory()`, `voterRegistry.ts#registerVoterFactory`). Pourquoi un registre et pas un scan DI des
543
546
  `@injectable` : les interfaces TS sont **effacées à la compilation** — rien à scanner ; le registre
544
- **est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts:6-16`). Trois axes (rôles,
547
+ **est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts#IVoterFactoryContext`). Trois axes (rôles,
545
548
  scopes, métier), un même jury, combinables.
546
549
 
547
550
  ## 🔌 HTTP et WebSocket — le même firewall
548
551
 
549
- `Firewall.#wireRealtime()` (`firewall.ts:301`) câble, pour toute zone protégée `realtime !== false`
550
- (opt-out, `firewall.ts:278`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts:313`)
551
- **et** un `frameAuthorizer` (RBAC par canal, `firewall.ts:361`). Même résolution de zone que HTTP.
552
+ `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) câble, pour toute zone protégée `realtime !== false`
553
+ (opt-out, `firewall.ts#Firewall.#build`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts#Firewall.#wireRealtime`)
554
+ **et** un `frameAuthorizer` (RBAC par canal, `firewall.ts#Firewall.#wireRealtime`). Même résolution de zone que HTTP.
552
555
  Sur une socket, un refus n'a pas d'en-tête `WWW-Authenticate` (`Firewall.#setChallenge()`,
553
- `firewall.ts:1345`) : le **code de fermeture** suffit.
556
+ `firewall.ts#Firewall.#setChallenge`) : le **code de fermeture** suffit.
554
557
 
555
558
  ## 🛡️ En-têtes de sécurité, CSRF, CORS
556
559
 
557
- - **`Firewall.applySecurityHeaders()`** (`firewall.ts:1152`) : CSP, Referrer-Policy, COOP/COEP/CORP
558
- au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts:1081`) :
560
+ - **`Firewall.applySecurityHeaders()`** (`firewall.ts#Firewall.applySecurityHeaders`) : CSP, Referrer-Policy, COOP/COEP/CORP
561
+ au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts#Firewall.enforceCsrf`) :
559
562
  alloué seulement si une directive en a besoin.
560
- - **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts:1042`) : Fetch Metadata
561
- (`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts:977`), puis double-submit `x-csrf-token` ≡
562
- cookie + HMAC (`firewall.ts:628`).
563
- - **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts:1098`).
563
+ - **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts#Firewall.enforceCsrf`) : Fetch Metadata
564
+ (`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts#Firewall.#startTrace`), puis double-submit `x-csrf-token` ≡
565
+ cookie + HMAC (`firewall.ts#Firewall.describe`).
566
+ - **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts#Firewall.handleCors`).
564
567
 
565
568
  ## 📜 Normes appliquées
566
569
 
567
- | Domaine | Norme | Ancrage |
568
- | ---------------------- | --------------- | ------------------------------------------------------ |
569
- | Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts:1345`) |
570
- | Bearer | RFC 6750 | `JwtAuthenticator.ts:13` · `ApiKeyAuthenticator.ts:11` |
571
- | JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts:33-44,104-108` |
572
- | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts:10-28` |
573
- | Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts:838-841`) |
574
- | Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts:43-46,101-104` |
575
- | CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts:1042`) |
576
- | Modèle | Zero Trust | `firewall.ts:819-834` (aucune preuve → 401) |
570
+ | Domaine | Norme | Ancrage |
571
+ | ---------------------- | --------------- | ----------------------------------------------------------------- |
572
+ | Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts#Firewall.#setChallenge`) |
573
+ | Bearer | RFC 6750 | `JwtAuthenticator.ts` · `ApiKeyAuthenticator.ts#INVALID_TOKEN` |
574
+ | JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts#JwtAuthenticator,104-108` |
575
+ | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts` |
576
+ | Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts#Firewall.handleSecurity`) |
577
+ | Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts#UserPasswordAuthenticator,101-104` |
578
+ | CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts#Firewall.enforceCsrf`) |
579
+ | Modèle | Zero Trust | `firewall.ts#Firewall` (aucune preuve → 401) |
577
580
 
578
581
  ## ⚡ Performance & mémoire
579
582