@nodefony/security 10.0.0-beta.2 → 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 (249) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +48 -16
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
  4. package/dist/nodefony/command/security-oauth-doctor.js +68 -0
  5. package/dist/nodefony/command/security-secrets.js +52 -29
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +10 -7
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +9 -14
  10. package/dist/nodefony/config/config.js +99 -8
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/contracts/index.js +1 -2
  14. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  15. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  16. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  17. package/dist/nodefony/errors/CsrfError.js +1 -2
  18. package/dist/nodefony/errors/InsecureTransportError.js +23 -0
  19. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  20. package/dist/nodefony/errors/SsrfError.js +1 -2
  21. package/dist/nodefony/errors/ThrottledError.js +1 -2
  22. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  23. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  24. package/dist/nodefony/errors/index.js +2 -1
  25. package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
  26. package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
  27. package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
  28. package/dist/nodefony/keycloak/scaffold.js +231 -0
  29. package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
  30. package/dist/nodefony/scaffold/contribute.js +19 -0
  31. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  32. package/dist/nodefony/service/apiKeys.js +45 -5
  33. package/dist/nodefony/service/auditService.js +4 -5
  34. package/dist/nodefony/service/authFlow.js +118 -16
  35. package/dist/nodefony/service/authorization.js +1 -2
  36. package/dist/nodefony/service/cors.js +1 -2
  37. package/dist/nodefony/service/csrf.js +1 -2
  38. package/dist/nodefony/service/firewall.js +116 -25
  39. package/dist/nodefony/service/keycloak.js +208 -0
  40. package/dist/nodefony/service/oauth2.js +403 -28
  41. package/dist/nodefony/service/securityHeaders.js +1 -2
  42. package/dist/nodefony/service/tokenService.js +94 -27
  43. package/dist/nodefony/service/totp.js +55 -11
  44. package/dist/nodefony/service/webAuthn.js +24 -7
  45. package/dist/nodefony/service/webhooks.js +4 -5
  46. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  47. package/dist/nodefony/src/SecuredArea.js +15 -5
  48. package/dist/nodefony/src/accountBinding.js +96 -0
  49. package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
  50. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  51. package/dist/nodefony/src/admin/adminAudit.js +1 -3
  52. package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
  53. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  54. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  55. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  56. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  57. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  58. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  59. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  60. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  61. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  62. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
  63. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  64. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
  65. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
  66. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  67. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
  68. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
  69. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  70. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  71. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  72. package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
  73. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  74. package/dist/nodefony/src/csp.js +1 -2
  75. package/dist/nodefony/src/csrfToken.js +1 -2
  76. package/dist/nodefony/src/entryPoint.js +38 -0
  77. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  78. package/dist/nodefony/src/net/transportFailure.js +57 -0
  79. package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
  80. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  81. package/dist/nodefony/src/oauth/metadata.js +28 -12
  82. package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
  83. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
  84. package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
  85. package/dist/nodefony/src/oauth/providerRoles.js +78 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +48 -12
  87. package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
  88. package/dist/nodefony/src/openToSignup.js +46 -0
  89. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  90. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  91. package/dist/nodefony/src/rolesWithin.js +57 -0
  92. package/dist/nodefony/src/sessionAuthentication.js +25 -0
  93. package/dist/nodefony/src/sessionIdentity.js +29 -3
  94. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  95. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  96. package/dist/nodefony/src/token/JwtKeystore.js +89 -48
  97. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  98. package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
  99. package/dist/nodefony/src/token/UserToken.js +1 -2
  100. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  101. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  102. package/dist/nodefony/src/token/secretFile.js +60 -4
  103. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  104. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  105. package/dist/nodefony/src/token/tokenSort.js +1 -2
  106. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  107. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  108. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  109. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  110. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  111. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  112. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  113. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
  114. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  115. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  116. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  117. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  118. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  119. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  120. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  124. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  125. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  126. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  127. package/dist/types/index.d.ts +26 -6
  128. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
  129. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  130. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  131. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  132. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  133. package/dist/types/nodefony/config/config.d.ts +64 -1
  134. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  135. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  136. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  137. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  138. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  139. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  140. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  141. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  142. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  143. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  144. package/dist/types/nodefony/errors/index.d.ts +1 -0
  145. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  146. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  147. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  148. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  149. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  150. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  151. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  152. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  153. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  154. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  155. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  156. package/dist/types/nodefony/service/oauth2.d.ts +145 -9
  157. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  158. package/dist/types/nodefony/service/totp.d.ts +28 -4
  159. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  160. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  161. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  162. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
  163. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
  164. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  165. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
  166. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  167. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  168. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  169. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  170. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  171. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  172. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  173. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  174. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  175. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  176. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  177. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  178. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  179. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  180. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  181. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  182. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  183. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  184. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  185. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  186. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  187. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  188. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  189. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  190. package/docs/api-keys.md +182 -175
  191. package/docs/audit.md +141 -141
  192. package/docs/authenticators.md +129 -96
  193. package/docs/authorization.md +116 -69
  194. package/docs/cors.md +64 -64
  195. package/docs/csrf.md +77 -72
  196. package/docs/external-jwt.md +83 -11
  197. package/docs/firewall.md +119 -83
  198. package/docs/headers.md +82 -82
  199. package/docs/index.md +16 -8
  200. package/docs/keycloak.md +835 -0
  201. package/docs/login-page.md +361 -0
  202. package/docs/oauth2.md +291 -166
  203. package/docs/obtenir-un-jeton.md +131 -34
  204. package/docs/tokens.md +197 -140
  205. package/docs/totp.md +177 -136
  206. package/docs/webauthn.md +157 -154
  207. package/docs/webhooks.md +128 -125
  208. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  209. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  210. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  211. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  212. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  213. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  214. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  215. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  216. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  217. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  218. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  219. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  220. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  221. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  222. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  223. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  224. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  225. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  226. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  227. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  228. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  229. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  230. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  231. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  232. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  233. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  234. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  235. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  236. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  237. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  238. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  239. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  240. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  241. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  242. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  243. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  244. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  245. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  246. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  247. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  248. package/package.json +19 -17
  249. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/realtime/UserRealtimeToken.ts
1
+ // nodefony/src/realtime/UserRealtimeToken.ts
2
2
  /** Aucun scope — figé et partagé (0 allocation pour le cas humain, le plus courant). */
3
3
  const EMPTY_SCOPES = Object.freeze([]);
4
4
  /**
@@ -41,12 +41,20 @@ var UserRealtimeToken = class {
41
41
  */
42
42
  #scopes;
43
43
  /**
44
+ * Confirmation du COMPTE (annuaire relu), fournie par le
45
+ * {@link FirewallRealtimeAuthenticator} pour les modes dont l'application
46
+ * émet elle-même l'artefact. `null` = rien à confirmer.
47
+ */
48
+ #confirmAccount;
49
+ /**
44
50
  * @param user - identité authentifiée résolue par le firewall.
45
51
  * @param revalidate - preuve de vie de l'identité, adaptée à son mode.
46
52
  * @param type - mode d'authentification réel (défaut `"session"`).
47
53
  * @param scopes - scopes délégués du jeton d'origine (défaut aucun).
54
+ * @param confirmAccount - relecture du compte, payée sur `api.request` seul.
48
55
  */
49
- constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES) {
56
+ constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES, confirmAccount = null) {
57
+ this.#confirmAccount = confirmAccount;
50
58
  this.#user = user;
51
59
  this.#revalidate = revalidate;
52
60
  this.type = type;
@@ -67,6 +75,13 @@ var UserRealtimeToken = class {
67
75
  isValid(nowMs) {
68
76
  return this.#revalidate ? this.#revalidate(nowMs) : Promise.resolve(true);
69
77
  }
78
+ /**
79
+ * Le compte qui a ouvert la socket existe-t-il toujours, actif, et sous le
80
+ * même `id` immuable ? `true` si aucune confirmation n'est câblée.
81
+ */
82
+ confirmAccount() {
83
+ return this.#confirmAccount ? this.#confirmAccount() : Promise.resolve(true);
84
+ }
70
85
  getUserIdentifier() {
71
86
  return this.#user.identifier;
72
87
  }
@@ -90,5 +105,4 @@ var UserRealtimeToken = class {
90
105
  return key === "user" ? this.#user : void 0;
91
106
  }
92
107
  };
93
- //#endregion
94
108
  export { UserRealtimeToken, UserRealtimeToken as default };
@@ -1,5 +1,5 @@
1
1
  import { NODEFONY_CHANNEL_NAMESPACE, PLATFORM_CHANNELS, PLATFORM_INBOUND, startsWithCI } from "nodefony";
2
- //#region nodefony/src/realtime/frameAuthorizer.ts
2
+ // nodefony/src/realtime/frameAuthorizer.ts
3
3
  /**
4
4
  * Politique système par défaut des canaux d'**introspection serveur** : réservés
5
5
  * à l'exploitant de l'INSTANCE, pas à l'administrateur d'une application.
@@ -289,5 +289,4 @@ function buildFrameAuthorizer(firewall, options) {
289
289
  return true;
290
290
  };
291
291
  }
292
- //#endregion
293
292
  export { DEFAULT_SYSTEM_PREFIXES, DEFAULT_SYSTEM_RULES, RESERVED_FLOOR_PREFIXES, SECURITY_CHANNEL_POLICY, SYSTEM_CHANNEL_POLICY, UPLINK_CHANNEL_POLICY, buildFrameAuthorizer, buildFrameAuthorizer as default, buildSystemRules };
@@ -0,0 +1,57 @@
1
+ // nodefony/src/rolesWithin.ts
2
+ /**
3
+ * Lit un compte de l'annuaire, sans faire confiance au fournisseur : une
4
+ * lecture qui lève et une lecture qui rend `null` disent toutes deux
5
+ * « introuvable ».
6
+ */
7
+ async function loadAccount(users, identifier) {
8
+ try {
9
+ return await users.loadUserByIdentifier(identifier) ?? null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ /** Rôles chaîne d'un compte lu, sans lui faire confiance. */
15
+ function rolesOf(user) {
16
+ return Array.isArray(user.roles) ? user.roles.filter((r) => typeof r === "string") : [];
17
+ }
18
+ /**
19
+ * La règle « jouer un autre compte » : les rôles de `target`, hiérarchie
20
+ * comprise, sont-ils TOUS atteints par ceux de `owner` ? Jouer un compte ne
21
+ * monte jamais en droits.
22
+ *
23
+ * Fermée en cas de doute : un annuaire absent, un compte introuvable — y
24
+ * compris un sujet externe éphémère, qui n'a pas de compte local — rendent un
25
+ * refus, jamais un accord. L'annuaire est résolu à l'appel : le service `users`
26
+ * peut être posé après le pare-feu.
27
+ *
28
+ * @param resolveUsers - rend l'annuaire des comptes (`users`), ou `null`
29
+ * @param hierarchy - la hiérarchie de rôles du pare-feu
30
+ * @returns la règle, à poser au conteneur sous `ROLES_WITHIN`
31
+ */
32
+ function rolesWithin(resolveUsers, hierarchy) {
33
+ return async (owner, target) => {
34
+ const users = resolveUsers();
35
+ if (users === null) return {
36
+ ok: false,
37
+ reason: "annuaire des comptes indisponible (@nodefony/user)"
38
+ };
39
+ const ownerUser = await loadAccount(users, owner);
40
+ if (ownerUser === null) return {
41
+ ok: false,
42
+ reason: `\`${owner}\` n'a pas de compte dans l'annuaire de l'application (un sujet externe éphémère ne peut jouer aucun compte)`
43
+ };
44
+ const targetUser = await loadAccount(users, target);
45
+ if (targetUser === null) return {
46
+ ok: false,
47
+ reason: `compte \`${target}\` introuvable`
48
+ };
49
+ const granted = hierarchy.reachableRoles(rolesOf(ownerUser));
50
+ const extra = [...hierarchy.reachableRoles(rolesOf(targetUser))].filter((role) => !granted.has(role));
51
+ return extra.length === 0 ? { ok: true } : {
52
+ ok: false,
53
+ reason: `\`${target}\` a des rôles que \`${owner}\` n'a pas : ${extra.sort().join(", ")}`
54
+ };
55
+ };
56
+ }
57
+ export { rolesWithin };
@@ -0,0 +1,25 @@
1
+ // nodefony/src/sessionAuthentication.ts
2
+ /**
3
+ * Facteurs d'authentification de Nodefony → méthodes RFC 8176, posées dans la
4
+ * session à son ouverture. Les clés et leur lecteur sont au cœur
5
+ * (`readSessionAuthentication`, `nodefony`) ; seule la POLITIQUE de
6
+ * correspondance vit ici.
7
+ */
8
+ const FACTOR_AMR = Object.freeze({
9
+ password: Object.freeze(["pwd"]),
10
+ totp: Object.freeze(["pwd", "otp"]),
11
+ recovery: Object.freeze(["pwd", "otp"]),
12
+ webauthn: Object.freeze(["pop"])
13
+ });
14
+ const NO_LOCAL_FACTOR = Object.freeze([]);
15
+ /**
16
+ * Valeurs RFC 8176 d'un facteur nommé par un chemin d'authentification.
17
+ *
18
+ * @param factor - le facteur tel que l'audit le nomme (`password`, `totp`,
19
+ * `recovery`, `webauthn`, `oauth`…)
20
+ * @returns les méthodes vérifiées localement ; vide pour un facteur délégué
21
+ */
22
+ function amrForFactor(factor) {
23
+ return Object.hasOwn(FACTOR_AMR, factor) ? FACTOR_AMR[factor] ?? NO_LOCAL_FACTOR : NO_LOCAL_FACTOR;
24
+ }
25
+ export { amrForFactor };
@@ -1,6 +1,7 @@
1
1
  import { AuthenticationError } from "../errors/AuthenticationError.js";
2
+ import { isBoundToAccount } from "./accountBinding.js";
2
3
  import { UserNotFoundError } from "@nodefony/user";
3
- //#region nodefony/src/sessionIdentity.ts
4
+ // nodefony/src/sessionIdentity.ts
4
5
  const INVALID_SESSION = "Invalid session";
5
6
  /**
6
7
  * Résout l'identité portée par une session (l'identifiant stocké dans le blob)
@@ -31,5 +32,30 @@ async function resolveSessionIdentity(provider, identifier) {
31
32
  if (user.isLocked() || !user.isActive()) throw new AuthenticationError(INVALID_SESSION);
32
33
  return user;
33
34
  }
34
- //#endregion
35
- export { resolveSessionIdentity };
35
+ /**
36
+ * Résout l'identité d'une session DÉJÀ ouverte, en exigeant qu'elle ait été
37
+ * ouverte pour CE compte — {@link resolveSessionIdentity} plus la liaison à
38
+ * l'identifiant immuable (`isBoundToAccount`).
39
+ *
40
+ * La session range son principal sous l'`identifier`, qu'un compte recréé
41
+ * (suppression hors framework, restauration, import) peut reprendre : sans la
42
+ * liaison, la session de l'ancien titulaire ouvrirait le compte du nouveau.
43
+ * Une session sans liaison est refusée au même titre.
44
+ *
45
+ * @param provider - source d'identité (`UserService` via le container).
46
+ * @param identifier - identifiant fonctionnel stocké en session.
47
+ * @param boundAccountId - identifiant immuable inscrit à l'ouverture
48
+ * (`metaBag`, clé `ACCOUNT_ID_KEY`).
49
+ * @param boundCredentialsVersion - version des identifiants inscrite à
50
+ * l'ouverture (`metaBag`, clé `CREDENTIALS_VERSION_KEY`).
51
+ * @returns l'utilisateur actif, titulaire de la session.
52
+ * @throws AuthenticationError (401, message uniforme) — identifiant inconnu,
53
+ * compte verrouillé ou désactivé, session ouverte pour un autre compte ou
54
+ * avant le dernier changement de mot de passe.
55
+ */
56
+ async function resolveBoundSessionIdentity(provider, identifier, boundAccountId, boundCredentialsVersion) {
57
+ const user = await resolveSessionIdentity(provider, identifier);
58
+ if (!isBoundToAccount(user, boundAccountId, boundCredentialsVersion)) throw new AuthenticationError(INVALID_SESSION);
59
+ return user;
60
+ }
61
+ export { resolveBoundSessionIdentity, resolveSessionIdentity };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/throttle/LoginThrottler.ts
1
+ // nodefony/src/throttle/LoginThrottler.ts
2
2
  const DEFAULT_FREE_ATTEMPTS = 3;
3
3
  const DEFAULT_BASE_DELAY_S = 1;
4
4
  const DEFAULT_CAP_DELAY_S = 900;
@@ -93,5 +93,4 @@ var LoginThrottler = class {
93
93
  }
94
94
  }
95
95
  };
96
- //#endregion
97
96
  export { LoginThrottler, LoginThrottler as default };
@@ -1,5 +1,5 @@
1
1
  import { anonymousUser } from "@nodefony/user";
2
- //#region nodefony/src/token/AnonymousToken.ts
2
+ // nodefony/src/token/AnonymousToken.ts
3
3
  /**
4
4
  * Token du visiteur non authentifié — Zero Trust : un visiteur EST un utilisateur
5
5
  * anonyme (jamais `null`).
@@ -36,5 +36,4 @@ var AnonymousToken = class {
36
36
  (this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
37
37
  }
38
38
  };
39
- //#endregion
40
39
  export { AnonymousToken, AnonymousToken as default };
@@ -1,6 +1,7 @@
1
- import { messageNonRestreint, modeNonRestreintAsync, readIfPresent, writeSecret } from "./secretFile.js";
1
+ import { createSecretExclusive, messageNonRestreint, modeNonRestreintAsync, readIfPresent } from "./secretFile.js";
2
+ import { isPlainObject } from "nodefony";
2
3
  import { join } from "node:path";
3
- //#region nodefony/src/token/JwtKeystore.ts
4
+ // nodefony/src/token/JwtKeystore.ts
4
5
  /**
5
6
  * Dossiers que le `Dockerfile` généré EFFACE à l'étage de construction.
6
7
  *
@@ -36,6 +37,67 @@ function keystoreLeaksIntoImage(dir) {
36
37
  return `JWT keystore: « ${dir} » n'est PAS sous ${CLEANED_DIRS.map((d) => `\`${d}/\``).join(" ni ")}, les seuls dossiers que le Dockerfile généré efface avant de fabriquer l'image. La clé privée de signature partira donc dans ton image de conteneur, où elle reste lisible par quiconque la télécharge — même effacée par une couche suivante. Deux issues : déplacer le dossier sous \`var/\` (ex. \`var/keys\`), ou injecter la clé par jwt.keystore.keySetJson depuis l'environnement, ce qui est la voie de production.`;
37
38
  }
38
39
  /**
40
+ * Lit un jeu de clés sérialisé et en vérifie la FORME — la seule lecture du
41
+ * dépôt, appelée par le keystore ET par la validation de configuration.
42
+ *
43
+ * Ne vérifie que la structure (`keys` non vide, un `kid` par clé) : importer
44
+ * les clés exige `jose`, chargé paresseusement au premier jeton. Les messages
45
+ * ne recopient JAMAIS la valeur reçue — elle porte une clé privée.
46
+ *
47
+ * @param json - le jeu de clés, tel que `NF_JWT_KEYSET` ou `keyset.json` le porte.
48
+ * @returns le jeu de clés lu.
49
+ * @throws Error si le JSON est illisible ou la structure incomplète.
50
+ */
51
+ function parseKeySet(json) {
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(json);
55
+ } catch {
56
+ throw new Error("JwtKeystore: keyset JSON invalide");
57
+ }
58
+ if (!isStoredKeySet(parsed)) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Forme minimale d'un jeu de clés relu : un objet dont `keys` est un tableau non
63
+ * vide d'objets portant chacun un `kid` textuel. Le reste de chaque JWK est
64
+ * éprouvé par `jose` à l'import de la clé.
65
+ */
66
+ function isStoredKeySet(value) {
67
+ if (!isPlainObject(value)) return false;
68
+ const keys = value.keys;
69
+ return Array.isArray(keys) && keys.length > 0 && keys.every((k) => isPlainObject(k) && typeof k.kid === "string");
70
+ }
71
+ /** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
72
+ async function buildKeySet(jose) {
73
+ const { publicKey, privateKey } = await jose.generateKeyPair("Ed25519", { extractable: true });
74
+ const publicJwk = await jose.exportJWK(publicKey);
75
+ const kid = await jose.calculateJwkThumbprint(publicJwk, "sha256");
76
+ return {
77
+ active: kid,
78
+ keys: [{
79
+ ...await jose.exportJWK(privateKey),
80
+ kid,
81
+ alg: "EdDSA",
82
+ use: "sig",
83
+ createdAt: Date.now()
84
+ }]
85
+ };
86
+ }
87
+ /**
88
+ * Génère un jeu de clés de signature à partager entre tous les process d'une
89
+ * application — la valeur de `NF_JWT_KEYSET`.
90
+ *
91
+ * Même générateur que celui du keystore (une seule implémentation), sérialisé
92
+ * en JSON COMPACT : une ligne, sans quote simple, qu'un `.env` porte entre
93
+ * quotes simples et qu'un gestionnaire de secrets stocke tel quel.
94
+ *
95
+ * @returns le jeu de clés sérialisé — il contient la clé PRIVÉE.
96
+ */
97
+ async function generateKeySet() {
98
+ return JSON.stringify(await buildKeySet(await import("jose")));
99
+ }
100
+ /**
39
101
  * Keystore Ed25519 — implémentation de référence d'{@link IJwtKeystore}.
40
102
  *
41
103
  * Résout la clé de signature selon une **priorité** (jamais d'auto-génération en
@@ -55,10 +117,15 @@ function keystoreLeaksIntoImage(dir) {
55
117
  * usage ; le boot ne paie rien si le JWT n'est jamais sollicité. Le chargement
56
118
  * est mémoïsé (une seule résolution concurrente).
57
119
  *
58
- * @remarks Race au 1ᵉʳ boot d'un **cluster** sans clé pré-provisionnée : deux
59
- * workers peuvent générer puis écrire des clés différentes (le dernier `rename`
60
- * gagne). En prod, provisionner la clé hors-bande (`keySetJson`/SecretProvider
61
- * P16) élimine ce cas — c'est précisément la source recommandée.
120
+ * @remarks Plusieurs process qui servent la MÊME application doivent signer
121
+ * avec la même clé, sinon un jeton signé par l'un est refusé par l'autre :
122
+ * - **pods** (aucun disque partagé) → source `env` obligatoire, la même valeur
123
+ * injectée partout (`NF_JWT_KEYSET`, générée par `security:secrets`) ;
124
+ * - **workers de `nodefony cluster`** → la source `env` est héritée au fork ; à
125
+ * défaut, la source `fichier` est sûre : la création de `keyset.json` est
126
+ * EXCLUSIVE ({@link createSecretExclusive}), un seul worker la génère et les
127
+ * autres relisent la sienne ;
128
+ * - source `mémoire` → une clé par process, jamais cohérente.
62
129
  */
63
130
  var JwtKeystore = class {
64
131
  #source;
@@ -91,7 +158,7 @@ var JwtKeystore = class {
91
158
  async #load() {
92
159
  const jose = await import("jose");
93
160
  if (this.#source.keySetJson) {
94
- await this.#importKeyset(jose, this.#parseKeyset(this.#source.keySetJson));
161
+ await this.#importKeyset(jose, parseKeySet(this.#source.keySetJson));
95
162
  return;
96
163
  }
97
164
  if (this.#source.dir) {
@@ -101,32 +168,24 @@ var JwtKeystore = class {
101
168
  const existing = await this.#readFile(file);
102
169
  if (existing) {
103
170
  await this.#checkRestricted(file);
104
- await this.#importKeyset(jose, this.#parseKeyset(existing));
171
+ await this.#importKeyset(jose, parseKeySet(existing));
172
+ return;
173
+ }
174
+ const keyset = await buildKeySet(jose);
175
+ if (await createSecretExclusive(file, JSON.stringify(keyset))) {
176
+ this.#log(`JWT keystore: clé Ed25519 générée et persistée (${file}).`, "INFO");
177
+ await this.#checkRestricted(file);
178
+ await this.#importKeyset(jose, keyset);
105
179
  return;
106
180
  }
107
- const keyset = await this.#generate(jose);
108
- await this.#writeAtomic(file, JSON.stringify(keyset));
109
- await this.#importKeyset(jose, keyset);
181
+ const winner = await this.#readFile(file);
182
+ if (winner === null) throw new Error(`JwtKeystore: ${file} a disparu juste après sa création`);
183
+ await this.#checkRestricted(file);
184
+ await this.#importKeyset(jose, parseKeySet(winner));
110
185
  return;
111
186
  }
112
- this.#log("JWT keystore: clé de signature ÉPHÉMÈRE en mémoire (perdue au redémarrage → refresh tokens invalidés, incohérente en cluster). Configurez jwt.keystore.dir (dev/VPS) ou jwt.keystore.keySetJson depuis l'env (prod).", "WARNING");
113
- await this.#importKeyset(jose, await this.#generate(jose));
114
- }
115
- /** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
116
- async #generate(jose) {
117
- const { publicKey, privateKey } = await jose.generateKeyPair("Ed25519", { extractable: true });
118
- const publicJwk = await jose.exportJWK(publicKey);
119
- const kid = await jose.calculateJwkThumbprint(publicJwk, "sha256");
120
- return {
121
- active: kid,
122
- keys: [{
123
- ...await jose.exportJWK(privateKey),
124
- kid,
125
- alg: "EdDSA",
126
- use: "sig",
127
- createdAt: Date.now()
128
- }]
129
- };
187
+ this.#log("JWT keystore: clé de signature ÉPHÉMÈRE en mémoire — perdue au redémarrage (jetons en vol invalidés) et propre à CE process : derrière plusieurs pods ou workers, un jeton signé par l'un est refusé par l'autre (401 au hasard). Production : poser jwt.keystore.keySetJson depuis l'environnement (NF_JWT_KEYSET dans une application générée — `npx nodefony security:secrets --jwt-keyset`). Développement : jwt.keystore.dir.", "WARNING");
188
+ await this.#importKeyset(jose, await buildKeySet(jose));
130
189
  }
131
190
  /** Importe un keyset persisté : privée → `CryptoKey`, public → JWK sans `d`. */
132
191
  async #importKeyset(jose, keyset) {
@@ -153,27 +212,10 @@ var JwtKeystore = class {
153
212
  this.#keys = loaded;
154
213
  this.#activeKid = keyset.active && loaded.some((k) => k.kid === keyset.active) ? keyset.active : firstKey.kid;
155
214
  }
156
- #parseKeyset(json) {
157
- let parsed;
158
- try {
159
- parsed = JSON.parse(json);
160
- } catch {
161
- throw new Error("JwtKeystore: keyset JSON invalide");
162
- }
163
- const keyset = parsed;
164
- if (!keyset || !Array.isArray(keyset.keys) || keyset.keys.length === 0 || keyset.keys.some((k) => typeof k.kid !== "string")) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
165
- return keyset;
166
- }
167
215
  /** Lit un fichier ; `null` si absent (ENOENT), relance toute autre erreur. */
168
216
  async #readFile(file) {
169
217
  return readIfPresent(file);
170
218
  }
171
- /** Écriture atomique (tmp + rename) en mode 600 — pas de fichier partiel lu. */
172
- async #writeAtomic(file, data) {
173
- await writeSecret(file, data);
174
- this.#log(`JWT keystore: clé Ed25519 générée et persistée (${file}).`, "INFO");
175
- await this.#checkRestricted(file);
176
- }
177
219
  /**
178
220
  * Constate le mode EFFECTIF du fichier de clés et avertit s'il n'est pas 0600.
179
221
  *
@@ -193,5 +235,4 @@ var JwtKeystore = class {
193
235
  this.#log(`JWT keystore: ${messageNonRestreint(file, mode)} La clé PRIVÉE y réside : provisionnez-la plutôt hors-bande via jwt.keystore.keySetJson (recommandé en production).`, "WARNING");
194
236
  }
195
237
  };
196
- //#endregion
197
- export { JwtKeystore, JwtKeystore as default, keystoreLeaksIntoImage };
238
+ export { JwtKeystore, JwtKeystore as default, generateKeySet, keystoreLeaksIntoImage, parseKeySet };
@@ -1,7 +1,7 @@
1
1
  import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./tokenSort.js";
2
2
  import { matchesTokenStatus } from "./tokenStatus.js";
3
3
  import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
4
- //#region nodefony/src/token/MemoryTokenStore.ts
4
+ // nodefony/src/token/MemoryTokenStore.ts
5
5
  /**
6
6
  * Filtre un record contre une requête de listing — prédicat de RÉFÉRENCE du
7
7
  * contrat, réutilisé par les implémentations qui évaluent en mémoire.
@@ -245,5 +245,4 @@ var MemoryTokenStore = class {
245
245
  for (const [jti, expiresAt] of this.#deniedJti) if (expiresAt <= now) this.#deniedJti.delete(jti);
246
246
  }
247
247
  };
248
- //#endregion
249
248
  export { MemoryTokenStore, matchesTokenQuery };
@@ -1,5 +1,6 @@
1
- import { canonicalIssuer, extractScopes, issuerMetadataUrls, validateIssuerMetadata } from "nodefony";
2
- //#region nodefony/src/token/RemoteJwtVerifier.ts
1
+ import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
2
+ import { canonicalIssuer, extractScopes, metadataCandidateUrls, parseDiscoveryUrl, validateIssuerMetadata } from "nodefony";
3
+ // nodefony/src/token/RemoteJwtVerifier.ts
3
4
  /**
4
5
  * Codes d'erreur `jose` qui désignent un **jeton fautif** — la liste est
5
6
  * BLANCHE, et c'est le point important.
@@ -65,8 +66,8 @@ var RemoteJwtVerifier = class {
65
66
  #jose = null;
66
67
  /**
67
68
  * @param options - émetteurs de confiance et réglages réseau
68
- * @throws Error si un émetteur est invalide, dupliqué, ou déclare un
69
- * algorithme à secret partagé
69
+ * @throws Error si un émetteur est invalide, dupliqué, déclare un
70
+ * algorithme à secret partagé ou une `discoveryUrl` invalide
70
71
  */
71
72
  constructor(options) {
72
73
  this.#options = options;
@@ -75,6 +76,7 @@ var RemoteJwtVerifier = class {
75
76
  const issuer = canonicalIssuer(trusted.issuer);
76
77
  if (this.#issuers.has(issuer)) throw new Error(`émetteur « ${issuer} » déclaré deux fois — deux politiques pour un même émetteur ne peuvent pas coexister : la seconde serait ignorée en silence.`);
77
78
  assertAsymmetric(issuer, trusted.algorithms);
79
+ if (trusted.discoveryUrl !== void 0) parseDiscoveryUrl(trusted.discoveryUrl);
78
80
  this.#issuers.set(issuer, {
79
81
  ...trusted,
80
82
  issuer
@@ -101,6 +103,36 @@ var RemoteJwtVerifier = class {
101
103
  * « jeton invalide »
102
104
  */
103
105
  async verify(token, audience) {
106
+ const verified = await this.verifyClaims(token, audience);
107
+ if (verified === null) return null;
108
+ const { issuer, payload } = verified;
109
+ return {
110
+ issuer,
111
+ subject: typeof payload.sub === "string" ? payload.sub : void 0,
112
+ scopes: extractScopes(payload),
113
+ expiresAt: typeof payload.exp === "number" ? payload.exp : void 0,
114
+ issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
115
+ tokenId: typeof payload.jti === "string" ? payload.jti : void 0,
116
+ claims: payload
117
+ };
118
+ }
119
+ /**
120
+ * Vérifie un jeton et rend ses claims BRUTS — pour un jeton qui n'est pas un
121
+ * jeton d'accès (jeton de déconnexion OpenID Connect, par exemple), dont le
122
+ * sens se lit dans des claims que {@link verify} ne remonte pas.
123
+ *
124
+ * Mêmes garanties et même contrat d'erreur que {@link verify} : signature,
125
+ * émetteur de la liste fermée, algorithme imposé, audience obligatoire,
126
+ * claims exigés par l'émetteur ; un refus est un `null`, une panne lève.
127
+ *
128
+ * @param token - le jeton brut, tel que présenté
129
+ * @param audience - audience que le jeton DOIT porter dans `aud`
130
+ * @returns l'émetteur canonique et les claims vérifiés, ou `null` si le jeton
131
+ * est refusé
132
+ * @throws Error si l'émetteur ne peut pas être joint ou publie un jeu de clés
133
+ * inutilisable
134
+ */
135
+ async verifyClaims(token, audience) {
104
136
  if (typeof token !== "string" || token.length === 0) return null;
105
137
  const jose = this.#jose ??= await import("jose");
106
138
  let claimedIssuer;
@@ -126,18 +158,16 @@ var RemoteJwtVerifier = class {
126
158
  ...trusted.typ ? { typ: trusted.typ } : {},
127
159
  ...trusted.requiredClaims?.length ? { requiredClaims: [...trusted.requiredClaims] } : {}
128
160
  });
129
- const subject = typeof payload.sub === "string" ? payload.sub : void 0;
130
161
  return {
131
162
  issuer: claimedIssuer,
132
- subject,
133
- scopes: extractScopes(payload),
134
- expiresAt: typeof payload.exp === "number" ? payload.exp : void 0,
135
- issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
136
- tokenId: typeof payload.jti === "string" ? payload.jti : void 0
163
+ payload
137
164
  };
138
165
  } catch (error) {
139
166
  const code = error.code;
140
- if (!code || !TOKEN_FAULT_CODES.has(code)) throw new Error(`vérification impossible : l'émetteur « ${trusted.issuer} » n'a pas fourni de jeu de clés utilisable (${code ?? error.message}).`, { cause: error });
167
+ if (!code || !TOKEN_FAULT_CODES.has(code)) {
168
+ const hint = untrustedAuthorityHint(error);
169
+ throw new Error(`vérification impossible : l'émetteur « ${trusted.issuer} » n'a pas fourni de jeu de clés utilisable (${code ?? describeTransportFailure(error)}).` + (hint === null ? "" : ` ${hint}.`), { cause: error });
170
+ }
141
171
  this.#audit(`jeton refusé (${trusted.issuer}) : ${code}`);
142
172
  return null;
143
173
  }
@@ -167,7 +197,7 @@ var RemoteJwtVerifier = class {
167
197
  trusted,
168
198
  getKey: jose.createLocalJWKSet(await trusted.localJwks())
169
199
  };
170
- const jwksUri = trusted.jwksUri ?? await this.#discover(trusted.issuer);
200
+ const jwksUri = trusted.jwksUri ?? await this.#discover(trusted);
171
201
  const options = {
172
202
  timeoutDuration: this.#options.timeoutMs ?? 5e3,
173
203
  cooldownDuration: this.#options.cooldownMs ?? 3e4,
@@ -188,11 +218,12 @@ var RemoteJwtVerifier = class {
188
218
  * « meilleure réponse que rien » : il est écarté comme s'il n'avait pas
189
219
  * répondu, et la recherche continue.
190
220
  */
191
- async #discover(issuer) {
221
+ async #discover(trusted) {
222
+ const { issuer, discoveryUrl } = trusted;
192
223
  const doFetch = this.#options.fetch ?? globalThis.fetch;
193
224
  const timeout = this.#options.timeoutMs ?? 5e3;
194
225
  const failures = [];
195
- for (const url of issuerMetadataUrls(issuer)) try {
226
+ for (const url of metadataCandidateUrls(issuer, discoveryUrl)) try {
196
227
  const response = await doFetch(url, {
197
228
  signal: AbortSignal.timeout(timeout),
198
229
  redirect: "manual",
@@ -202,7 +233,7 @@ var RemoteJwtVerifier = class {
202
233
  failures.push(`${url} → HTTP ${response.status}`);
203
234
  continue;
204
235
  }
205
- const metadata = validateIssuerMetadata(await response.json(), issuer);
236
+ const metadata = validateIssuerMetadata(await response.json(), issuer, discoveryUrl);
206
237
  this.#audit(`émetteur « ${issuer} » découvert : clés sur ${metadata.jwksUri}.`);
207
238
  return metadata.jwksUri;
208
239
  } catch (error) {
@@ -227,5 +258,4 @@ function assertAsymmetric(issuer, algorithms) {
227
258
  if (algorithms.length === 0) throw new Error(`émetteur « ${issuer} » : aucun algorithme accepté — la liste est la garde principale (RFC 8725 §3.1), elle ne peut pas être vide.`);
228
259
  for (const alg of algorithms) if (alg.startsWith("HS") || alg.toLowerCase() === "none") throw new Error(`émetteur « ${issuer} » : algorithme « ${alg} » refusé. Les clés proviennent d'un jeu PUBLIC : un algorithme à secret partagé y transformerait la clé publique en secret de signature.`);
229
260
  }
230
- //#endregion
231
261
  export { RemoteJwtVerifier, RemoteJwtVerifier as default };
@@ -1,5 +1,5 @@
1
1
  import { anonymousUser } from "@nodefony/user";
2
- //#region nodefony/src/token/UserToken.ts
2
+ // nodefony/src/token/UserToken.ts
3
3
  /**
4
4
  * Jeton porteur d'un utilisateur réel — produit par les authenticators à
5
5
  * credential (`userpassword`, puis `session`/`jwt`...).
@@ -63,5 +63,4 @@ var UserToken = class {
63
63
  (this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
64
64
  }
65
65
  };
66
- //#endregion
67
66
  export { UserToken, UserToken as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/jwtRuntime.ts
1
+ // nodefony/src/token/jwtRuntime.ts
2
2
  /**
3
3
  * Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
4
4
  * `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
@@ -27,5 +27,4 @@ function resolveJwtRuntime(jwt) {
27
27
  function defaultAudience(runtime) {
28
28
  return runtime.audiences[0] ?? runtime.issuer;
29
29
  }
30
- //#endregion
31
30
  export { defaultAudience, resolveJwtRuntime };