@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
@@ -3,7 +3,7 @@ import { AUDIT_FILTERS } from "../audit/auditFilters.js";
3
3
  import { adminActor, auditAdmin } from "./adminAudit.js";
4
4
  import { webhookAdminEndpoints } from "./WebhookAdminApi.js";
5
5
  import { parseFilters, parsePageQuery } from "nodefony";
6
- //#region nodefony/src/admin/SecurityAdminApi.ts
6
+ // nodefony/src/admin/SecurityAdminApi.ts
7
7
  /**
8
8
  * Déduit le **driver** logique d'un store à partir de son nom de classe — le
9
9
  * `tokenService` connaît le driver config (`tokenStore.store`) mais ne l'expose
@@ -99,6 +99,39 @@ function parseAuditQuery(query) {
99
99
  if (page.cursor !== void 0) filter.cursor = page.cursor;
100
100
  return filter;
101
101
  }
102
+ /**
103
+ * Traduit l'`id` d'un compte (paramètre `{id}` d'une route admin) en sa **clé de
104
+ * sujet** `IUser.identifier` — la clé sous laquelle vivent ses facteurs (TOTP,
105
+ * passkeys), sa session et ses jetons.
106
+ *
107
+ * Viser un store avec l'`id` brut agit sur une clé qui n'existe pas et rend un
108
+ * succès : l'administrateur croit avoir débloqué un compte resté bloqué. D'où
109
+ * le 503 quand le service `users` manque — sans lui, l'`id` ne se traduit pas.
110
+ *
111
+ * @param container - container du kernel (service `users`).
112
+ * @param id - paramètre `{id}` de la route.
113
+ * @returns le compte (`id` + `identifier`), ou la réponse d'erreur à rendre (404/503).
114
+ */
115
+ async function resolveSubject(container, id) {
116
+ if (typeof id !== "string" || id.length === 0) return {
117
+ status: 404,
118
+ body: { error: "not found" }
119
+ };
120
+ const users = container.get("users");
121
+ if (!users) return {
122
+ status: 503,
123
+ body: { error: "users unavailable" }
124
+ };
125
+ const user = await users.findById(id);
126
+ if (!user || typeof user.identifier !== "string") return {
127
+ status: 404,
128
+ body: { error: "not found" }
129
+ };
130
+ return {
131
+ id,
132
+ identifier: user.identifier
133
+ };
134
+ }
102
135
  /** Projette un credential serveur vers sa vue admin (sans `publicKey`/`userId`). */
103
136
  function toCredentialView(c) {
104
137
  return {
@@ -170,6 +203,25 @@ function createSecurityAdminApi(container) {
170
203
  return firewall.describe();
171
204
  }
172
205
  },
206
+ {
207
+ path: "oauth/diagnosis",
208
+ method: "GET",
209
+ role: "ROLE_NODEFONY_ADMIN",
210
+ summary: "Diagnostic des fournisseurs OAuth sans connexion humaine : émetteur, URL de retour, secret (?provider=<nom>). Interroge les fournisseurs à chaque appel ; aucun secret rendu.",
211
+ handler: async (request) => {
212
+ const oauth = container.get("oauth2");
213
+ if (!oauth || !oauth.isEnabled()) return {
214
+ enabled: false,
215
+ providers: []
216
+ };
217
+ const raw = request.query.provider;
218
+ const provider = typeof raw === "string" && raw !== "" ? raw : void 0;
219
+ return {
220
+ enabled: true,
221
+ providers: await oauth.diagnose(provider)
222
+ };
223
+ }
224
+ },
173
225
  {
174
226
  path: "roleHierarchy",
175
227
  method: "GET",
@@ -324,17 +376,9 @@ function createSecurityAdminApi(container) {
324
376
  status: 503,
325
377
  body: { error: "webauthn unavailable" }
326
378
  };
327
- const id = request.params.id;
328
- if (typeof id !== "string" || id.length === 0) return {
329
- status: 404,
330
- body: { error: "not found" }
331
- };
332
- const users = container.get("users");
333
- if (users && !await users.findById(id)) return {
334
- status: 404,
335
- body: { error: "not found" }
336
- };
337
- return { credentials: (await svc.listUserCredentials(id)).map(toCredentialView) };
379
+ const account = await resolveSubject(container, request.params.id);
380
+ if (!("identifier" in account)) return account;
381
+ return { credentials: (await svc.listUserCredentials(account.identifier)).map(toCredentialView) };
338
382
  }
339
383
  },
340
384
  {
@@ -348,13 +392,14 @@ function createSecurityAdminApi(container) {
348
392
  status: 503,
349
393
  body: { error: "webauthn unavailable" }
350
394
  };
351
- const id = request.params.id;
352
395
  const credentialId = request.params.credentialId;
353
- if (typeof id !== "string" || id.length === 0 || typeof credentialId !== "string" || credentialId.length === 0) return {
396
+ if (typeof credentialId !== "string" || credentialId.length === 0) return {
354
397
  status: 404,
355
398
  body: { error: "not found" }
356
399
  };
357
- if (!await svc.removeUserCredential(id, credentialId)) return {
400
+ const account = await resolveSubject(container, request.params.id);
401
+ if (!("identifier" in account)) return account;
402
+ if (!await svc.removeUserCredential(account.identifier, credentialId)) return {
358
403
  status: 404,
359
404
  body: { error: "not found" }
360
405
  };
@@ -363,7 +408,7 @@ function createSecurityAdminApi(container) {
363
408
  action: "user.passkey_revoked",
364
409
  outcome: "success",
365
410
  actor: adminActor(request.user),
366
- resource: id,
411
+ resource: account.id,
367
412
  metadata: {
368
413
  credentialId,
369
414
  viaAdmin: true
@@ -381,7 +426,8 @@ function createSecurityAdminApi(container) {
381
426
  const svc = container.get("totp");
382
427
  const pageQuery = parsePageQuery(request.query, {
383
428
  defaultLimit: KEYS_DEFAULT_LIMIT,
384
- maxLimit: KEYS_MAX_LIMIT
429
+ maxLimit: KEYS_MAX_LIMIT,
430
+ searchable: true
385
431
  });
386
432
  const limit = pageQuery.limit;
387
433
  const offset = pageQuery.offset ?? 0;
@@ -393,7 +439,7 @@ function createSecurityAdminApi(container) {
393
439
  offset
394
440
  };
395
441
  const confirmed = one(request.query, "confirmed");
396
- const q = one(request.query, "q");
442
+ const q = pageQuery.q;
397
443
  const page = await svc.listPage({
398
444
  limit,
399
445
  offset,
@@ -420,17 +466,9 @@ function createSecurityAdminApi(container) {
420
466
  status: 503,
421
467
  body: { error: "totp unavailable" }
422
468
  };
423
- const id = request.params.id;
424
- if (typeof id !== "string" || id.length === 0) return {
425
- status: 404,
426
- body: { error: "not found" }
427
- };
428
- const users = container.get("users");
429
- if (users && !await users.findById(id)) return {
430
- status: 404,
431
- body: { error: "not found" }
432
- };
433
- return svc.status(id);
469
+ const account = await resolveSubject(container, request.params.id);
470
+ if (!("identifier" in account)) return account;
471
+ return svc.status(account.identifier);
434
472
  }
435
473
  },
436
474
  {
@@ -444,23 +482,15 @@ function createSecurityAdminApi(container) {
444
482
  status: 503,
445
483
  body: { error: "totp unavailable" }
446
484
  };
447
- const id = request.params.id;
448
- if (typeof id !== "string" || id.length === 0) return {
449
- status: 404,
450
- body: { error: "not found" }
451
- };
452
- const users = container.get("users");
453
- if (users && !await users.findById(id)) return {
454
- status: 404,
455
- body: { error: "not found" }
456
- };
457
- await svc.disable(id);
485
+ const account = await resolveSubject(container, request.params.id);
486
+ if (!("identifier" in account)) return account;
487
+ await svc.disable(account.identifier);
458
488
  auditAdmin(container, {
459
489
  category: "auth",
460
490
  action: "user.totp_disabled",
461
491
  outcome: "success",
462
492
  actor: adminActor(request.user),
463
- resource: id,
493
+ resource: account.id,
464
494
  metadata: { viaAdmin: true }
465
495
  });
466
496
  return { ok: true };
@@ -492,5 +522,4 @@ function registerSecurityAdminApi(registry, container) {
492
522
  if (registry.has("security")) return;
493
523
  registry.register(createSecurityAdminApi(container));
494
524
  }
495
- //#endregion
496
525
  export { createSecurityAdminApi, parseAuditQuery, parseTokenListQuery, registerSecurityAdminApi };
@@ -1,7 +1,7 @@
1
1
  import { WEBHOOK_FACETS, WEBHOOK_FILTERS, WEBHOOK_STATS_FILTERS } from "../webhook/webhookFilters.js";
2
2
  import { adminActor, auditAdmin } from "./adminAudit.js";
3
- import { parseFilters, parsePageQuery } from "nodefony";
4
- //#region nodefony/src/admin/WebhookAdminApi.ts
3
+ import { isPlainObject, parseFilters, parsePageQuery } from "nodefony";
4
+ // nodefony/src/admin/WebhookAdminApi.ts
5
5
  /**
6
6
  * Déduit le driver logique du store webhook depuis son nom de classe (miroir de
7
7
  * `tokenStoreDriver`). Fermé sur les implémentations connues ; `null` pour un
@@ -30,6 +30,21 @@ function bodyStringArray(v) {
30
30
  if (!Array.isArray(v) || v.length === 0) return void 0;
31
31
  return v.every((x) => typeof x === "string" && x.length > 0) ? v : void 0;
32
32
  }
33
+ /**
34
+ * Le corps d'une requête d'écriture : un objet JSON, ou rien (lu comme `{}`).
35
+ * Un tableau ou un scalaire rend `null` — l'appelant répond 400 au lieu de
36
+ * laisser un `in` lever sur une chaîne.
37
+ */
38
+ function bodyObject(request) {
39
+ const body = request.body;
40
+ if (body === void 0 || body === null) return {};
41
+ return isPlainObject(body) ? body : null;
42
+ }
43
+ /** Réponse au corps qui n'est pas un objet JSON. */
44
+ const BODY_NOT_OBJECT = {
45
+ status: 400,
46
+ body: { error: "JSON object body required" }
47
+ };
33
48
  /** Page par défaut du listing d'endpoints (registre de config, volume modeste). */
34
49
  const ENDPOINTS_DEFAULT_LIMIT = 50;
35
50
  /** Plafond dur : un client ne peut pas demander « tout » via `?limit=`. */
@@ -75,8 +90,8 @@ function parseWebhookListQuery(query, sortable) {
75
90
  * fail-closed, et logge).
76
91
  */
77
92
  function mapWebhookError(e) {
78
- if (e.code === 422) {
79
- const message = e.message;
93
+ if (typeof e === "object" && e !== null && "code" in e && e.code === 422) {
94
+ const message = "message" in e ? e.message : void 0;
80
95
  return {
81
96
  status: 422,
82
97
  body: { error: typeof message === "string" ? message : "invalid url" }
@@ -170,7 +185,8 @@ function webhookAdminEndpoints(container) {
170
185
  handler: async (request) => {
171
186
  const s = svc();
172
187
  if (!ready(s)) return UNAVAILABLE;
173
- const body = request.body ?? {};
188
+ const body = bodyObject(request);
189
+ if (body === null) return BODY_NOT_OBJECT;
174
190
  const url = bodyString(body.url);
175
191
  if (!url) return {
176
192
  status: 400,
@@ -252,7 +268,8 @@ function webhookAdminEndpoints(container) {
252
268
  if (!ready(s)) return UNAVAILABLE;
253
269
  const id = pathId(request);
254
270
  if (!id) return NOT_FOUND;
255
- const body = request.body ?? {};
271
+ const body = bodyObject(request);
272
+ if (body === null) return BODY_NOT_OBJECT;
256
273
  const patch = {};
257
274
  const fields = [];
258
275
  if ("url" in body) {
@@ -375,5 +392,4 @@ function webhookAdminEndpoints(container) {
375
392
  }
376
393
  ];
377
394
  }
378
- //#endregion
379
395
  export { parseWebhookListQuery, webhookAdminEndpoints };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/admin/adminAudit.ts
1
+ // nodefony/src/admin/adminAudit.ts
2
2
  /**
3
3
  * Helpers d'audit PARTAGÉS par les producteurs admin du module sécurité
4
4
  * (`SecurityAdminApi`, `WebhookAdminApi`…). Extraits dans leur propre fichier
@@ -16,7 +16,6 @@
16
16
  function adminActor(user) {
17
17
  if (user && typeof user === "object") {
18
18
  const u = user;
19
- if (typeof u.username === "string" && u.username) return u.username;
20
19
  if (typeof u.identifier === "string" && u.identifier) return u.identifier;
21
20
  }
22
21
  return "admin";
@@ -33,5 +32,4 @@ function adminActor(user) {
33
32
  function auditAdmin(container, draft) {
34
33
  container.get("auditService")?.record?.(draft);
35
34
  }
36
- //#endregion
37
35
  export { adminActor, auditAdmin };
@@ -1,40 +1,168 @@
1
- import { USER_REVOKED_EVENT } from "@nodefony/user";
2
- //#region nodefony/src/admin/userRevocationCascade.ts
1
+ import { USER_CREATED_EVENT, USER_REVOKED_EVENT } from "@nodefony/user";
2
+ // nodefony/src/admin/userRevocationCascade.ts
3
+ /** Garde : la valeur sait-elle journaliser (`log` est une fonction) ? */
4
+ function isKernelLogger(value) {
5
+ return typeof value === "object" && value !== null && "log" in value && typeof value.log === "function";
6
+ }
7
+ /**
8
+ * Journalise l'échec d'une brique de la cascade. Le geste reste best-effort —
9
+ * une brique en panne n'empêche pas les autres — mais il ne peut plus échouer
10
+ * EN SILENCE : un accès qui survit à sa révocation (session encore ouverte
11
+ * après un changement de mot de passe) doit laisser une trace qui dit laquelle.
12
+ *
13
+ * @param container - container du kernel (service `kernel`).
14
+ * @param brick - brique en échec (`sessions`, `tokenStore`, `totp`, `webauthn`).
15
+ * @param identifier - compte visé par la cascade.
16
+ * @param error - erreur levée par la brique.
17
+ */
18
+ function reportCascadeFailure(container, brick, identifier, error) {
19
+ try {
20
+ const kernel = container.get("kernel");
21
+ if (!isKernelLogger(kernel)) return;
22
+ kernel.log(`révocation de "${identifier}" incomplète : la brique ${brick} a échoué — ` + (error instanceof Error ? error.message : String(error)), "ERROR", "SECURITY");
23
+ } catch {}
24
+ }
3
25
  /**
4
26
  * Cascade de révocation déclenchée par {@link USER_REVOKED_EVENT} : éjecte
5
27
  * **immédiatement** les artefacts d'accès du porteur — ses **sessions** (http,
6
28
  * `destroyByUser`) et ses **jetons/PAT** (`tokenStore.revokeAllForSubject`,
7
- * seuil `invalidBefore`). Best-effort par brique (une indispo n'empêche pas
8
- * l'autre) — l'accès était DÉJÀ neutralisé par le re-fetch des authenticators,
9
- * cette cascade est de la **propreté + défense en profondeur**, jamais l'unique
10
- * rempart. `tenantId` du payload est réservé (scoping non câblé en mono-tenant).
29
+ * seuil `invalidBefore`, qui couvre aussi les clés d'API). Best-effort par
30
+ * brique (une indispo n'empêche pas l'autre) — l'accès était DÉJÀ neutralisé par
31
+ * le re-fetch des authenticators. `tenantId` du payload est réservé (scoping non
32
+ * câblé en mono-tenant).
33
+ *
34
+ * **Suppression du compte** (`reason: "deleted"`) : retire AUSSI ses facteurs —
35
+ * secret TOTP (codes de récupération compris) et passkeys. Ils sont rangés sous
36
+ * l'`identifier`, qu'un compte recréé peut reprendre : laissés en place, le
37
+ * nouveau titulaire hériterait du second facteur de l'ancien. Un compte
38
+ * seulement désactivé ou verrouillé les GARDE — il les retrouve à sa
39
+ * réactivation.
11
40
  *
12
- * @param container - container du kernel (résolution lazy de `sessions`/`tokenStore`).
41
+ * @param container - container du kernel (résolution lazy de `sessions`, `tokenStore`, `totp`, `webauthn`).
13
42
  * @param event - charge utile de l'événement (porteur + raison).
14
43
  * @param now - horloge (epoch ms) injectable pour les tests.
15
44
  */
16
45
  async function cascadeUserRevocation(container, event, now = Date.now()) {
17
46
  const identifier = event.identifier;
47
+ await revokeAccessArtifacts(container, identifier, now);
48
+ if (event.reason === "deleted") await removeAccountFactors(container, identifier);
49
+ }
50
+ /**
51
+ * Éjecte les artefacts d'accès rangés sous un identifiant — ses sessions (http,
52
+ * `destroyByUser`) et ses jetons/PAT/clés d'API (`tokenStore.revokeAllForSubject`,
53
+ * seuil `invalidBefore`). SEULE implémentation du geste, appelée à la révocation
54
+ * d'un compte ET à sa création. Best-effort par brique : l'indisponibilité de
55
+ * l'une n'empêche pas l'autre.
56
+ *
57
+ * @param container - container du kernel (services `sessions`, `tokenStore`).
58
+ * @param identifier - `IUser.identifier` du compte.
59
+ * @param invalidBefore - seuil (epoch ms) : tout jeton émis avant est rejeté.
60
+ */
61
+ async function revokeAccessArtifacts(container, identifier, invalidBefore) {
18
62
  try {
19
63
  await container.get("sessions")?.destroyByUser?.(identifier);
20
- } catch {}
64
+ } catch (error) {
65
+ reportCascadeFailure(container, "sessions", identifier, error);
66
+ }
21
67
  try {
22
- await container.get("tokenStore")?.revokeAllForSubject(identifier, now);
23
- } catch {}
68
+ await container.get("tokenStore")?.revokeAllForSubject(identifier, invalidBefore);
69
+ } catch (error) {
70
+ reportCascadeFailure(container, "tokenStore", identifier, error);
71
+ }
72
+ }
73
+ /**
74
+ * Purge, à la NAISSANCE d'un compte, tout ce qu'un ancien titulaire du même
75
+ * identifiant a laissé derrière lui : sessions, jetons et facteurs. Un compte
76
+ * qui vient de naître n'a encore rien émis — tout artefact déjà rangé sous son
77
+ * identifiant appartient donc à quelqu'un d'autre. C'est ce qui ferme le cas
78
+ * d'un compte supprimé HORS du framework (`DELETE` SQL, aucun événement), dont
79
+ * l'accès redeviendrait valide dès qu'un compte renaît sous son nom.
80
+ *
81
+ * Le seuil est tronqué à la SECONDE : l'`iat` d'un JWT l'est aussi (RFC 7519
82
+ * §2, NumericDate), et un seuil à la milliseconde rejetterait le jeton du
83
+ * nouveau titulaire émis dans la même seconde que sa création.
84
+ *
85
+ * @param container - container du kernel.
86
+ * @param identifier - `IUser.identifier` du compte créé.
87
+ * @param now - horloge (epoch ms) injectable pour les tests.
88
+ */
89
+ async function purgeInheritedAccess(container, identifier, now = Date.now()) {
90
+ await revokeAccessArtifacts(container, identifier, Math.floor(now / 1e3) * 1e3);
91
+ await removeAccountFactors(container, identifier);
92
+ }
93
+ /**
94
+ * Retire les facteurs rangés sous un identifiant — secret TOTP (codes de
95
+ * récupération compris) et passkeys. SEULE implémentation du geste, appelée à
96
+ * la suppression d'un compte ET à la création d'un compte sous cet identifiant.
97
+ * Best-effort par brique : un TOTP indisponible n'empêche pas le retrait des
98
+ * passkeys.
99
+ *
100
+ * @param container - container du kernel (services `totp`, `webauthn`).
101
+ * @param identifier - `IUser.identifier` du compte.
102
+ */
103
+ async function removeAccountFactors(container, identifier) {
104
+ try {
105
+ const totp = container.get("totp");
106
+ if (totp?.isEnabled?.() !== false) await totp?.disable?.(identifier);
107
+ } catch (error) {
108
+ reportCascadeFailure(container, "totp", identifier, error);
109
+ }
110
+ try {
111
+ const webauthn = container.get("webauthn");
112
+ if (webauthn?.isEnabled?.() !== false) {
113
+ const creds = await webauthn?.listUserCredentials?.(identifier) ?? [];
114
+ for (const cred of creds) await webauthn?.removeUserCredential?.(identifier, cred.id);
115
+ }
116
+ } catch (error) {
117
+ reportCascadeFailure(container, "webauthn", identifier, error);
118
+ }
119
+ }
120
+ /** Garde : charge utile de {@link USER_CREATED_EVENT} (identifiant non vide). */
121
+ function isUserCreatedEvent(value) {
122
+ return typeof value === "object" && value !== null && "identifier" in value && typeof value.identifier === "string" && value.identifier.length > 0;
24
123
  }
25
124
  /**
26
- * Abonne la cascade au bus kernel. À appeler au `onKernelBoot` d'un module
27
- * bootable (ici `@nodefony/security`). **Extensible** : tout autre module
28
- * (webhooks…) peut s'abonner au MÊME `USER_REVOKED_EVENT` pour ses propres
29
- * artefacts, sans toucher à ce fichier.
125
+ * Abonne la cascade au bus kernel, UNE fois, au `onKernelBoot` de
126
+ * `@nodefony/security` — avant qu'aucun compte ne puisse être créé ou révoqué
127
+ * (les comptes semés au démarrage compris). Deux abonnements :
128
+ * - {@link USER_REVOKED_EVENT} → {@link cascadeUserRevocation} ;
129
+ * - {@link USER_CREATED_EVENT} → {@link purgeInheritedAccess} : sessions,
130
+ * jetons et facteurs HÉRITÉS. Un compte qui vient de naître n'a encore rien
131
+ * émis ni enrôlé, donc tout artefact déjà rangé sous son identifiant
132
+ * appartient à un ancien titulaire. Règle exacte, aucune date à comparer.
133
+ * ⚠️ Un import qui chargerait les facteurs AVANT les comptes les verrait
134
+ * retirés : importer les comptes d'abord.
135
+ *
136
+ * Les deux sont émis par `UserService` seul, et attendus : chaque handler rend
137
+ * sa promesse. **Extensible** : tout autre module (webhooks…) s'abonne aux
138
+ * MÊMES événements pour ses propres artefacts, sans toucher à ce fichier.
30
139
  *
31
140
  * @param kernel - bus d'événements (kernel) exposant `on`.
32
- * @param container - container capturé par le handler.
141
+ * @param container - container capturé par les handlers.
33
142
  */
34
143
  function registerUserRevocationCascade(kernel, container) {
35
- kernel.on(USER_REVOKED_EVENT, (event) => {
36
- cascadeUserRevocation(container, event);
37
- });
144
+ kernel.on(USER_REVOKED_EVENT, (event) => cascadeUserRevocation(container, event));
145
+ kernel.on(USER_CREATED_EVENT, (event) => isUserCreatedEvent(event) ? purgeInheritedAccess(container, event.identifier) : void 0);
146
+ }
147
+ /**
148
+ * Avis de démarrage quand le service `users` n'émet pas sur le bus du kernel —
149
+ * construit sans container (`new UserService(repo, encoder)`). Ses événements
150
+ * partent alors sur son propre bus, où personne n'écoute : supprimer un compte
151
+ * laisserait ses sessions, ses jetons et ses facteurs en place, SANS UN MOT.
152
+ *
153
+ * @param container - container du kernel.
154
+ * @param kernel - le kernel attendu comme bus des événements.
155
+ * @returns l'avis à déclarer, ou `null` si `users` est absent ou bien câblé.
156
+ */
157
+ function usersEventBusNotice(container, kernel) {
158
+ const users = container.get("users");
159
+ if (typeof users !== "object" || users === null) return null;
160
+ if (!("kernel" in users) || users.kernel === kernel) return null;
161
+ return {
162
+ code: "USERS_SERVICE_WITHOUT_KERNEL",
163
+ level: "warning",
164
+ message: "le service « users » n'atteint pas le kernel : supprimer, désactiver un compte ou changer son mot de passe ne révoquera ni ses sessions, ni ses jetons, ni ses facteurs.",
165
+ fix: "new UserService(repository, encoder, module.container)"
166
+ };
38
167
  }
39
- //#endregion
40
- export { cascadeUserRevocation, registerUserRevocationCascade };
168
+ export { cascadeUserRevocation, purgeInheritedAccess, registerUserRevocationCascade, removeAccountFactors, usersEventBusNotice };
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- //#region nodefony/src/apikey/apiKeyFormat.ts
2
+ // nodefony/src/apikey/apiKeyFormat.ts
3
3
  /**
4
4
  * Encodage / décodage / hachage des clés API (PAT) — logique **pure** (aucun I/O,
5
5
  * aucun container) → testable sans serveur et partagée par l'émetteur
@@ -103,5 +103,4 @@ function parseApiKey(token, prefix) {
103
103
  secretHash: hashApiKey(token)
104
104
  };
105
105
  }
106
- //#endregion
107
106
  export { generateApiKey, hashApiKey, looksLikeApiKey, parseApiKey };
@@ -1,5 +1,5 @@
1
1
  import { assertPageQuery } from "nodefony";
2
- //#region nodefony/src/audit/MemoryAuditStore.ts
2
+ // nodefony/src/audit/MemoryAuditStore.ts
3
3
  const DEFAULT_MAX_ENTRIES = 1e4;
4
4
  const MAX_LIMIT = 500;
5
5
  /**
@@ -113,5 +113,4 @@ var MemoryAuditStore = class {
113
113
  for (const event of snapshot.events) this.#events.push(event);
114
114
  }
115
115
  };
116
- //#endregion
117
116
  export { MemoryAuditStore };
@@ -1,5 +1,5 @@
1
1
  import { PLATFORM_CHANNELS } from "nodefony";
2
- //#region nodefony/src/audit/auditBridge.ts
2
+ // nodefony/src/audit/auditBridge.ts
3
3
  /**
4
4
  * Canal WS du flux live d'audit (P6.14 lot 4). Le préfixe `security:` le place
5
5
  * sous le plancher `SECURITY_CHANNEL_POLICY` (ROLE_NODEFONY_ADMIN) du verrou de
@@ -82,5 +82,4 @@ function createAuditBridge(source, publish, channel, opts = {}) {
82
82
  unsubscribe();
83
83
  };
84
84
  }
85
- //#endregion
86
85
  export { SECURITY_AUDIT_CHANNEL, createAuditBridge, createAuditBridge as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/audit/auditFilters.ts
1
+ // nodefony/src/audit/auditFilters.ts
2
2
  /**
3
3
  * **Le vocabulaire de filtre du journal d'audit**, en noms PUBLICS — ceux qu'un
4
4
  * auditeur écrit dans l'URL (`?category=authz&outcome=denied&since=…`).
@@ -56,5 +56,4 @@ const AUDIT_FILTERS = {
56
56
  * les deux à jour », qui avait échoué en silence.
57
57
  */
58
58
  const AUDIT_FILTER_VOCABULARY_IS_COMPLETE = [true, true];
59
- //#endregion
60
59
  export { AUDIT_FILTERS, AUDIT_FILTER_VOCABULARY_IS_COMPLETE };
@@ -1,5 +1,5 @@
1
1
  import { MemoryAuditStore } from "./MemoryAuditStore.js";
2
- //#region nodefony/src/audit/auditStoreRegistry.ts
2
+ // nodefony/src/audit/auditStoreRegistry.ts
3
3
  const factories = /* @__PURE__ */ new Map();
4
4
  /**
5
5
  * Enregistre (ou remplace) la fabrique d'un store d'audit. Appelée par le builtin
@@ -21,5 +21,4 @@ registerAuditStore("memory", (ctx) => {
21
21
  const days = ctx?.config?.audit?.retentionDays;
22
22
  return typeof days === "number" ? new MemoryAuditStore(Date.now, days * MS_PER_DAY) : new MemoryAuditStore();
23
23
  });
24
- //#endregion
25
24
  export { getAuditStoreFactory, listAuditStores, registerAuditStore };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/audit/readAuditContext.ts
1
+ // nodefony/src/audit/readAuditContext.ts
2
2
  /**
3
3
  * Extrait IP / User-Agent / requestId + drapeaux de présence (jamais la valeur)
4
4
  * d'un contexte de requête — pour enrichir un événement d'audit avec la
@@ -20,5 +20,4 @@ function readAuditContext(context) {
20
20
  }
21
21
  };
22
22
  }
23
- //#endregion
24
23
  export { readAuditContext };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/audit/recordAudit.ts
1
+ // nodefony/src/audit/recordAudit.ts
2
2
  /**
3
3
  * Émet un événement d'audit **si** le service est présent — no-op sinon. La
4
4
  * résolution se fait par le container (`auditService`) sur le **cold-path**
@@ -12,5 +12,4 @@
12
12
  function recordAudit(container, event) {
13
13
  (container?.get("auditService"))?.record(event);
14
14
  }
15
- //#endregion
16
15
  export { recordAudit };
@@ -1,5 +1,5 @@
1
1
  import { AnonymousToken } from "../token/AnonymousToken.js";
2
- //#region nodefony/src/authenticator/AnonymousAuthenticator.ts
2
+ // nodefony/src/authenticator/AnonymousAuthenticator.ts
3
3
  /**
4
4
  * Acceptation EXPLICITE de l'anonymat dans une zone — le seul authenticator
5
5
  * autorisé à produire un token non authentifié sans déclencher le Zero Trust.
@@ -32,5 +32,4 @@ var AnonymousAuthenticator = class {
32
32
  return Promise.resolve();
33
33
  }
34
34
  };
35
- //#endregion
36
35
  export { AnonymousAuthenticator, AnonymousAuthenticator as default };
@@ -1,8 +1,9 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { isBoundToAccount } from "../accountBinding.js";
2
3
  import { UserToken } from "../token/UserToken.js";
3
4
  import { bearerToken } from "./bearer.js";
4
5
  import { looksLikeApiKey, parseApiKey } from "../apikey/apiKeyFormat.js";
5
- //#region nodefony/src/authenticator/ApiKeyAuthenticator.ts
6
+ // nodefony/src/authenticator/ApiKeyAuthenticator.ts
6
7
  const INVALID_TOKEN = "Invalid token";
7
8
  /**
8
9
  * Marqueur interne posé par `authenticate()` et consommé par `onSuccess()` :
@@ -81,6 +82,7 @@ var ApiKeyAuthenticator = class {
81
82
  const invalidBefore = await store.getInvalidBefore(record.subjectId);
82
83
  if (invalidBefore !== null && record.createdAt < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
83
84
  const user = await this.#resolveUserOrReject(record.subjectId);
85
+ if (!isBoundToAccount(user, record.metadata["accountId"], record.metadata["credentialsVersion"])) throw new AuthenticationError(INVALID_TOKEN);
84
86
  const last = record.lastUsedAt;
85
87
  if (this.#throttleMs === 0 || last === null || now - last >= this.#throttleMs) token.setAttribute(MARK_USED_AT, now);
86
88
  const ut = token;
@@ -160,5 +162,4 @@ var ApiKeyAuthenticator = class {
160
162
  return this.#userProvider;
161
163
  }
162
164
  };
163
- //#endregion
164
165
  export { ApiKeyAuthenticator, ApiKeyAuthenticator as default };