@nodefony/security 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +182 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +151 -0
  6. package/dist/nodefony/command/security-secrets.js +158 -0
  7. package/dist/nodefony/command/security-token.js +335 -0
  8. package/dist/nodefony/command/security-user-add.js +131 -0
  9. package/dist/nodefony/command/security-user-delete.js +102 -0
  10. package/dist/nodefony/command/security-user-list.js +77 -0
  11. package/dist/nodefony/config/config.js +366 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +35 -0
  13. package/dist/nodefony/contracts/IAccessVoter.js +13 -0
  14. package/dist/nodefony/contracts/IApiKey.js +1 -0
  15. package/dist/nodefony/contracts/IAuditEvent.js +1 -0
  16. package/dist/nodefony/contracts/IAuditStore.js +1 -0
  17. package/dist/nodefony/contracts/IAuthenticator.js +1 -0
  18. package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
  19. package/dist/nodefony/contracts/IFirewall.js +1 -0
  20. package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
  21. package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
  22. package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
  23. package/dist/nodefony/contracts/ISecuredArea.js +1 -0
  24. package/dist/nodefony/contracts/IToken.js +1 -0
  25. package/dist/nodefony/contracts/ITokenStore.js +1 -0
  26. package/dist/nodefony/contracts/ITotpSecret.js +1 -0
  27. package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
  28. package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
  29. package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
  30. package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
  31. package/dist/nodefony/contracts/IWebhookStore.js +1 -0
  32. package/dist/nodefony/contracts/index.js +2 -0
  33. package/dist/nodefony/errors/AccessDeniedError.js +14 -0
  34. package/dist/nodefony/errors/ApiKeyError.js +21 -0
  35. package/dist/nodefony/errors/AuthenticationError.js +14 -0
  36. package/dist/nodefony/errors/CsrfError.js +23 -0
  37. package/dist/nodefony/errors/InvalidTargetError.js +39 -0
  38. package/dist/nodefony/errors/SsrfError.js +17 -0
  39. package/dist/nodefony/errors/ThrottledError.js +21 -0
  40. package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
  41. package/dist/nodefony/errors/WebAuthnError.js +21 -0
  42. package/dist/nodefony/errors/index.js +9 -0
  43. package/dist/nodefony/service/accessTokenVerifier.js +77 -0
  44. package/dist/nodefony/service/apiKeys.js +310 -0
  45. package/dist/nodefony/service/auditService.js +145 -0
  46. package/dist/nodefony/service/authFlow.js +332 -0
  47. package/dist/nodefony/service/authorization.js +95 -0
  48. package/dist/nodefony/service/cors.js +81 -0
  49. package/dist/nodefony/service/csrf.js +97 -0
  50. package/dist/nodefony/service/firewall.js +699 -0
  51. package/dist/nodefony/service/oauth2.js +153 -0
  52. package/dist/nodefony/service/securityHeaders.js +80 -0
  53. package/dist/nodefony/service/tokenService.js +486 -0
  54. package/dist/nodefony/service/totp.js +209 -0
  55. package/dist/nodefony/service/webAuthn.js +343 -0
  56. package/dist/nodefony/service/webhooks.js +539 -0
  57. package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
  58. package/dist/nodefony/src/SecuredArea.js +51 -0
  59. package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
  60. package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
  61. package/dist/nodefony/src/admin/adminAudit.js +37 -0
  62. package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
  63. package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
  64. package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
  65. package/dist/nodefony/src/audit/auditBridge.js +82 -0
  66. package/dist/nodefony/src/audit/auditFilters.js +60 -0
  67. package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
  68. package/dist/nodefony/src/audit/readAuditContext.js +24 -0
  69. package/dist/nodefony/src/audit/recordAudit.js +16 -0
  70. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
  71. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
  72. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
  73. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
  74. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
  75. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
  76. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
  77. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
  78. package/dist/nodefony/src/authenticator/bearer.js +2 -0
  79. package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
  80. package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
  81. package/dist/nodefony/src/crypto/secretCipher.js +79 -0
  82. package/dist/nodefony/src/csp.js +54 -0
  83. package/dist/nodefony/src/csrfToken.js +65 -0
  84. package/dist/nodefony/src/net/ssrfGuard.js +130 -0
  85. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +65 -0
  87. package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
  88. package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
  89. package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
  90. package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
  91. package/dist/nodefony/src/sessionIdentity.js +35 -0
  92. package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
  93. package/dist/nodefony/src/token/AnonymousToken.js +40 -0
  94. package/dist/nodefony/src/token/JwtKeystore.js +160 -0
  95. package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
  96. package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
  97. package/dist/nodefony/src/token/UserToken.js +67 -0
  98. package/dist/nodefony/src/token/jwtRuntime.js +19 -0
  99. package/dist/nodefony/src/token/secretFile.js +134 -0
  100. package/dist/nodefony/src/token/tokenCriteria.js +35 -0
  101. package/dist/nodefony/src/token/tokenFilters.js +72 -0
  102. package/dist/nodefony/src/token/tokenSort.js +40 -0
  103. package/dist/nodefony/src/token/tokenStatus.js +35 -0
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
  106. package/dist/nodefony/src/totp/totpCipher.js +30 -0
  107. package/dist/nodefony/src/totp/totpCrypto.js +226 -0
  108. package/dist/nodefony/src/totp/totpOperations.js +129 -0
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
  110. package/dist/nodefony/src/voter/RoleVoter.js +32 -0
  111. package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
  112. package/dist/nodefony/src/voter/voterRegistry.js +20 -0
  113. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
  114. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
  115. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
  116. package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
  117. package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
  118. package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
  119. package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
  120. package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
  121. package/dist/nodefony/src/webhook/webhookSort.js +48 -0
  122. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
  123. package/dist/types/index.d.ts +157 -0
  124. package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
  125. package/dist/types/nodefony/command/security-token.d.ts +44 -0
  126. package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
  127. package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
  128. package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
  129. package/dist/types/nodefony/config/config.d.ts +295 -0
  130. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  131. package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
  132. package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
  133. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
  134. package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
  135. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
  136. package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
  137. package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
  138. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
  139. package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
  140. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
  141. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
  142. package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
  143. package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
  144. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
  145. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
  146. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
  147. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
  148. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
  149. package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
  150. package/dist/types/nodefony/contracts/index.d.ts +9 -0
  151. package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
  152. package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
  153. package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
  154. package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
  155. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
  156. package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
  157. package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
  158. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
  159. package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
  160. package/dist/types/nodefony/errors/index.d.ts +8 -0
  161. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
  162. package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
  163. package/dist/types/nodefony/service/auditService.d.ts +30 -0
  164. package/dist/types/nodefony/service/authFlow.d.ts +123 -0
  165. package/dist/types/nodefony/service/authorization.d.ts +33 -0
  166. package/dist/types/nodefony/service/cors.d.ts +48 -0
  167. package/dist/types/nodefony/service/csrf.d.ts +57 -0
  168. package/dist/types/nodefony/service/firewall.d.ts +148 -0
  169. package/dist/types/nodefony/service/oauth2.d.ts +66 -0
  170. package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
  171. package/dist/types/nodefony/service/tokenService.d.ts +103 -0
  172. package/dist/types/nodefony/service/totp.d.ts +58 -0
  173. package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
  174. package/dist/types/nodefony/service/webhooks.d.ts +160 -0
  175. package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
  176. package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
  177. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
  178. package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
  179. package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
  180. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
  181. package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
  182. package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
  183. package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
  184. package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
  185. package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
  186. package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
  187. package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
  188. package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
  189. package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
  190. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
  191. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
  192. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
  193. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
  194. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
  195. package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
  196. package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
  197. package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
  198. package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
  199. package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
  200. package/dist/types/nodefony/src/csp.d.ts +39 -0
  201. package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
  202. package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
  203. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
  204. package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
  205. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
  206. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
  207. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
  208. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
  209. package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
  210. package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
  211. package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
  212. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
  213. package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
  214. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
  215. package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
  216. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
  217. package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
  218. package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
  219. package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
  220. package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
  221. package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
  222. package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
  223. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
  224. package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
  225. package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
  226. package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
  227. package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
  228. package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
  229. package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
  230. package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
  231. package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
  232. package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
  233. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
  234. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
  235. package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
  236. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
  237. package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
  238. package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
  239. package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
  240. package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
  241. package/docs/api-keys.md +691 -0
  242. package/docs/audit.md +751 -0
  243. package/docs/authenticators.md +487 -0
  244. package/docs/authorization.md +497 -0
  245. package/docs/cors.md +497 -0
  246. package/docs/csrf.md +392 -0
  247. package/docs/external-jwt.md +181 -0
  248. package/docs/firewall.md +546 -0
  249. package/docs/headers.md +616 -0
  250. package/docs/index.md +207 -0
  251. package/docs/lexique.md +190 -0
  252. package/docs/oauth2.md +575 -0
  253. package/docs/obtenir-un-jeton.md +225 -0
  254. package/docs/tokens.md +520 -0
  255. package/docs/totp.md +804 -0
  256. package/docs/webauthn.md +733 -0
  257. package/docs/webhooks.md +1016 -0
  258. package/package.json +83 -0
@@ -0,0 +1,335 @@
1
+ import { messageNonRestreint, modeNonRestreint, readIfPresentSync, writeSecretSync } from "../src/token/secretFile.js";
2
+ import { ADMIN_SCOPE_READ, ADMIN_SCOPE_WRITE, AGENT_TARGETS, Command, MCP_ENDPOINT_PATH, MCP_TOKEN_ENV, agentRoot, agentsPresents, alreadyHasKey, chargePrompts, poseVariable, requestedAgents } from "nodefony";
3
+ import path from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ import { spawnSync } from "node:child_process";
6
+ //#region nodefony/command/security-token.ts
7
+ const options = {
8
+ helpGroup: "COMPTES ET SECRETS",
9
+ showBanner: false,
10
+ kernelEvent: "onReady",
11
+ quietBoot: true
12
+ };
13
+ /**
14
+ * La table des agents, leurs emplacements de secret et la façon de leur
15
+ * déclarer la porte vivent dans le CŒUR (`nodefony/cli/agentTargets`) : ce sont
16
+ * `ai:mcp` et `create app` — des commandes du cœur — qui déclarent la porte,
17
+ * quand cette commande-ci pose le jeton. Deux tables recopiées auraient divergé
18
+ * au premier agent ajouté d'un seul côté, et la divergence se serait vue chez
19
+ * l'utilisateur, sous la forme d'un agent servi par l'une et ignoré par l'autre.
20
+ */
21
+ /** Plafond d'une durée demandée en ligne de commande : 30 jours. */
22
+ const TTL_MAX_MINUTES = 43200;
23
+ /**
24
+ * Traduit `--ttl` en secondes, ou rend l'erreur à afficher.
25
+ *
26
+ * Exportée pour être ÉPROUVÉE : c'est une fonction pure dont chaque verdict est
27
+ * binaire, et dont l'échec — une durée acceptée alors qu'elle est aberrante —
28
+ * ne se verrait qu'au moment où un jeton refuse de mourir.
29
+ *
30
+ * @param raw - la valeur telle que tapée, ou rien
31
+ * @returns les secondes, `undefined` si rien n'est demandé, une `Error` sinon
32
+ */
33
+ function ttlSeconds(raw) {
34
+ if (raw === void 0) return void 0;
35
+ const minutes = Number.parseInt(raw, 10);
36
+ if (!Number.isFinite(minutes) || minutes <= 0) return /* @__PURE__ */ new Error(`--ttl attend un nombre de MINUTES supérieur à zéro (reçu « ${raw} »)`);
37
+ if (minutes > TTL_MAX_MINUTES) return /* @__PURE__ */ new Error(`--ttl est borné à ${TTL_MAX_MINUTES} minutes (30 jours) — un jeton posé dans un fichier est une clé, et une clé se remplace`);
38
+ return minutes * 60;
39
+ }
40
+ const GREEN = "\x1B[32m";
41
+ const YELLOW = "\x1B[33m";
42
+ const DIM = "\x1B[2m";
43
+ const BOLD = "\x1B[1m";
44
+ const RESET = "\x1B[0m";
45
+ /**
46
+ * `nodefony security:token` — émet un jeton d'accès pour une porte de cette
47
+ * application (la porte MCP par défaut).
48
+ *
49
+ * **Pourquoi une commande, et pas un `curl`.** Le jeton s'obtenait par un appel
50
+ * au grant : trouver l'URL, composer un JSON, y mettre un mot de passe en clair
51
+ * dans l'historique du shell, et surtout AVOIR un serveur en marche. Personne ne
52
+ * fait ça deux fois. Ici l'application SIGNE elle-même — elle possède la clé —
53
+ * donc : pas de serveur, pas de mot de passe, pas de réseau.
54
+ *
55
+ * **L'audience est celle de la porte, et ce n'est pas un détail** (RFC 8707) :
56
+ * un jeton d'audience différente est refusé, à juste titre — c'est toute la
57
+ * raison d'être de la liaison d'audience. La commande la vise d'elle-même,
58
+ * `--resource` ne sert qu'à en viser une autre.
59
+ *
60
+ * Suit `security:secrets` : `--write` pose la valeur là où l'AGENT la lit
61
+ * (gitignoré), jamais dans le `.env` commité, et ne remplace jamais une valeur
62
+ * existante — une rotation est un geste explicite.
63
+ */
64
+ var SecurityToken = class extends Command {
65
+ constructor(cli) {
66
+ super("security:token", `émet un jeton d'accès pour la porte MCP`, cli, options);
67
+ this.addArgument("[identifier]", "compte porteur du jeton (défaut : admin)");
68
+ this.addOption("-s, --scope <scopes>", `scopes demandés, séparés par des espaces (défaut : « ${ADMIN_SCOPE_READ} » ; ajouter « ${ADMIN_SCOPE_WRITE} » pour les mutations)`);
69
+ this.addOption("-r, --resource <uri>", "audience visée (défaut : la porte MCP de cette application)");
70
+ this.addOption("-a, --agent <noms>", "agents à servir, séparés par des virgules (défaut : ceux détectés ; « none » pour aucun)");
71
+ this.addOption("-t, --ttl <duree>", "durée de validité, en minutes (défaut : celle de la config, 15 min)");
72
+ this.addOption("-w, --write", `pose ${MCP_TOKEN_ENV} dans la configuration des agents présents`);
73
+ this.addOption("-j, --json", "sortie JSON (scripts/CI)");
74
+ }
75
+ /** Racine du projet (le kernel la connaît ; repli sur le cwd). */
76
+ #root() {
77
+ return this.kernel?.path ?? process.cwd();
78
+ }
79
+ /** Ce fichier est-il couvert par un `.gitignore` ? */
80
+ #gitIgnored(file) {
81
+ try {
82
+ return spawnSync("git", [
83
+ "check-ignore",
84
+ "-q",
85
+ file
86
+ ], {
87
+ cwd: this.#root(),
88
+ stdio: "ignore"
89
+ }).status === 0;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ /** Ce fichier est-il SUIVI par git ? Un secret n'entre pas dans un suivi. */
95
+ #tracked(file) {
96
+ try {
97
+ return spawnSync("git", [
98
+ "ls-files",
99
+ "--error-unmatch",
100
+ file
101
+ ], {
102
+ cwd: this.#root(),
103
+ stdio: "ignore"
104
+ }).status === 0;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ /**
110
+ * Racine où vit la configuration d'une cible (projet, ou dossier maison).
111
+ *
112
+ * La résolution elle-même vit dans le cœur, avec la table : recopiée ici,
113
+ * elle aurait cessé d'honorer `CODEX_HOME`/`VIBE_HOME` le jour où l'une des
114
+ * deux copies aurait bougé — et le symptôme aurait été un jeton posé dans un
115
+ * dossier que l'agent ne lit pas, donc un 401 qui accuse le jeton.
116
+ */
117
+ #rootOf(target) {
118
+ return agentRoot(target, { projectRoot: this.#root() });
119
+ }
120
+ /** Contenu du fichier d'une cible, "" s'il n'existe pas. */
121
+ #contentOf(target) {
122
+ try {
123
+ const abs = path.resolve(this.#rootOf(target), target.file);
124
+ return readIfPresentSync(abs) ?? "";
125
+ } catch {
126
+ return "";
127
+ }
128
+ }
129
+ /**
130
+ * Agents dont la présence est CONSTATÉE — on ne crée pas la configuration
131
+ * d'un outil que personne n'utilise ici.
132
+ */
133
+ #agentsPresents() {
134
+ return agentsPresents({
135
+ projectRoot: this.#root(),
136
+ exists: existsSync
137
+ });
138
+ }
139
+ /**
140
+ * Pose le jeton dans la configuration des agents PRÉSENTS dans ce projet.
141
+ *
142
+ * Un agent n'est servi que si son marqueur existe : on ne crée pas la
143
+ * configuration d'un outil que personne n'utilise ici. Et jamais dans un
144
+ * fichier SUIVI par git — un jeton commité est un jeton publié, et c'est la
145
+ * seule faute de cette commande qui serait irrattrapable.
146
+ *
147
+ * @param token - le jeton à poser
148
+ * @param w - la sortie où rendre compte
149
+ * @param targets - agents à servir (déjà filtrés par le choix de l'appelant)
150
+ * @returns le nombre d'agents effectivement servis
151
+ */
152
+ #writeForAgents(token, w, targets) {
153
+ let servis = 0;
154
+ for (const target of targets) {
155
+ const root = this.#rootOf(target);
156
+ if (target.scope === "projet" && this.#tracked(target.file)) {
157
+ w(`${YELLOW}⚠ ${target.file} est SUIVI par git — rien n'est écrit.${RESET}\n${DIM} Un jeton commité est un jeton publié.${RESET}\n\n`);
158
+ continue;
159
+ }
160
+ const abs = path.resolve(root, target.file);
161
+ const pose = poseVariable(target.forme, readIfPresentSync(abs) ?? "", MCP_TOKEN_ENV, token);
162
+ if (pose instanceof Error) {
163
+ w(`${YELLOW}⚠ ${target.file} : ${pose.message} — rien n'est écrit.${RESET}\n\n`);
164
+ continue;
165
+ }
166
+ try {
167
+ writeSecretSync(abs, pose);
168
+ } catch (error) {
169
+ w(`${YELLOW}⚠ ${target.file} : écriture impossible — ${error.message}${RESET}\n\n`);
170
+ continue;
171
+ }
172
+ const resolvedMode = modeNonRestreint(abs);
173
+ if (typeof resolvedMode === "number") w(`${YELLOW}⚠ ${messageNonRestreint(abs, resolvedMode)}${RESET}\n`);
174
+ w(`${GREEN}✓ ${MCP_TOKEN_ENV} posé pour ${target.name}${RESET} ${DIM}(${target.scope === "projet" ? target.file : abs})${RESET}\n${DIM} RELANCE-le : il lit sa configuration au démarrage.${RESET}\n`);
175
+ if (target.scope === "projet" && !this.#gitIgnored(target.file)) w(`${YELLOW} ⚠ ${target.file} n'est PAS couvert par .gitignore — un « git add -A » l'emporterait.${RESET}\n`);
176
+ w("\n");
177
+ servis += 1;
178
+ }
179
+ return servis;
180
+ }
181
+ /**
182
+ * Audience par défaut : la porte MCP de CETTE application.
183
+ *
184
+ * Lue de la configuration quand elle y est écrite — c'est elle qui fait foi,
185
+ * et elle doit l'être : dérivée d'un en-tête `Host`, un `Host` forgé
186
+ * obtiendrait un jeton d'audience arbitraire. À défaut, on compose l'adresse
187
+ * locale, qui est celle du développement.
188
+ */
189
+ #defaultResource() {
190
+ const declaree = ((this.kernel?.modules)?.devkit?.options)?.mcp?.authorization?.resource;
191
+ if (typeof declaree === "string" && declaree.length > 0) return declaree;
192
+ return `http://localhost:${process.env.NF_PORT ?? "5151"}${MCP_ENDPOINT_PATH}`;
193
+ }
194
+ /**
195
+ * `true` si l'application signe avec une clé ÉPHÉMÈRE (ni `keySetJson` ni
196
+ * `dir` déclarés dans `security.jwt.keystore`).
197
+ *
198
+ * 🔴 C'est le piège que cette commande doit annoncer : le jeton produit est
199
+ * parfaitement valide et n'est vérifiable par PERSONNE d'autre que le process
200
+ * qui vient de le signer — celui-ci. Le serveur en marche a généré la sienne
201
+ * au démarrage, et refusera ce jeton en « autorisation requise ». Mesuré :
202
+ * deux `kid` distincts pour la même application, et un troisième après un
203
+ * redémarrage.
204
+ */
205
+ #ephemeralKey() {
206
+ const ks = (((this.kernel?.modules)?.security?.options)?.jwt)?.keystore;
207
+ return !ks?.keySetJson && !ks?.dir;
208
+ }
209
+ async generate(identifierArg, opts) {
210
+ const tokens = this.kernel?.container?.get("tokenService");
211
+ const users = this.kernel?.container?.get("users");
212
+ if (!tokens || !users) {
213
+ this.log(`service « ${!tokens ? "tokenService" : "users"} » absent — cette application ne provisionne pas d'émetteur de jetons.`, "ERROR");
214
+ process.exitCode = 1;
215
+ return this;
216
+ }
217
+ let identifier = identifierArg?.trim() ?? "";
218
+ if (!identifier && process.stdin.isTTY) {
219
+ const page = await users.listPage({ limit: 25 });
220
+ if (page.items.length > 0) {
221
+ await this.loadPrompts();
222
+ identifier = await this.prompts.select({
223
+ message: "Compte porteur du jeton :",
224
+ choices: page.items.map((u) => ({
225
+ name: `${u.identifier}${(u.roles ?? []).length ? ` ${DIM}(${(u.roles ?? []).join(", ")})${RESET}` : ""}`,
226
+ value: u.identifier
227
+ }))
228
+ });
229
+ }
230
+ }
231
+ if (!identifier) identifier = "admin";
232
+ const user = await users.findByIdentifier(identifier);
233
+ if (!user) {
234
+ this.log(`compte « ${identifier} » introuvable — crée-le : nodefony security:user:add ${identifier}`, "ERROR");
235
+ process.exitCode = 1;
236
+ return this;
237
+ }
238
+ const resource = opts.resource ?? this.#defaultResource();
239
+ const requestedScopes = (opts.scope ?? "").split(/\s+/u).filter(Boolean);
240
+ const scopes = requestedScopes.length > 0 ? requestedScopes : [ADMIN_SCOPE_READ];
241
+ const ttlS = ttlSeconds(opts.ttl);
242
+ if (ttlS instanceof Error) {
243
+ this.log(ttlS.message, "ERROR");
244
+ process.exitCode = 1;
245
+ return this;
246
+ }
247
+ let issued;
248
+ try {
249
+ issued = await tokens.issueTokens(user, scopes, resource, ttlS);
250
+ } catch (e) {
251
+ if (e.oauthError === "invalid_target") {
252
+ const env = this.kernel?.environment ?? "?";
253
+ const enDev = env === "development";
254
+ this.log(`impossible d'émettre un jeton pour cette porte ici.
255
+
256
+ Ce n'est PAS un problème de serveur : cette commande n'en a pas
257
+ besoin, elle signe le jeton elle-même.
258
+
259
+ La porte visée : ${resource}\n Environnement CONSTATÉ : ${env}\n\n` + (enDev ? ` Cette application n'accepte pas cette audience. Une audience se
260
+ DÉCLARE — c'est une liste blanche (RFC 8707), sans quoi tout
261
+ porteur obtiendrait un jeton pour la ressource de son choix :
262
+
263
+ use("@nodefony/security", {
264
+ jwt: { audiences: ["${resource}"] },\n })\n\n → puis npm run build (le runtime lit le dist)\n` : ` La porte est servie par un module de DÉVELOPPEMENT, absent en\n « ${env} » : un jeton pour une porte absente n'aurait personne\n pour l'accepter.\n\n → NODE_ENV=development nodefony security:token${opts.write ? " --write" : ""}\n`) + ` → ou vise une autre porte : --resource <uri>`, "ERROR");
265
+ process.exitCode = 1;
266
+ return this;
267
+ }
268
+ throw e;
269
+ }
270
+ const token = issued.access_token;
271
+ const accordes = (issued.scope ?? "").split(/\s+/u).filter(Boolean);
272
+ const nonAccordes = scopes.filter((s) => !accordes.includes(s));
273
+ if (opts.json) {
274
+ process.stdout.write(`${JSON.stringify({
275
+ access_token: token,
276
+ resource,
277
+ scopes: accordes,
278
+ requested: scopes,
279
+ expires_in: issued.expires_in
280
+ }, null, 2)}\n`);
281
+ return this;
282
+ }
283
+ const w = (s) => {
284
+ process.stdout.write(s);
285
+ };
286
+ let write = opts.write === true;
287
+ if (!write && process.stdin.isTTY && !opts.json) {
288
+ await this.loadPrompts();
289
+ write = await this.prompts.confirm({
290
+ message: `Poser ${MCP_TOKEN_ENV} dans la configuration des agents présents ?`,
291
+ default: true
292
+ });
293
+ }
294
+ if (this.#ephemeralKey()) w(`\n${YELLOW}⚠ Clé de signature ÉPHÉMÈRE — ce jeton sera REFUSÉ.${RESET}\n${DIM} Cette application n'a pas de clé persistante : chaque process en génère\n une au démarrage. Le jeton ci-dessous n'est vérifiable que par le process\n qui vient de le signer — pas par le serveur en marche, qui a la sienne.\n → déclare une source de clés dans nodefony.config.ts :\n use("@nodefony/security", { jwt: { keystore: { dir: "var/keys" } } })\n ou, en production, keySetJson depuis l'environnement.${RESET}\n`);
295
+ const minutes = Math.round((issued.expires_in ?? 0) / 60);
296
+ w(`\n${BOLD}🔑 Jeton d'accès${RESET} ${DIM}— compte ${identifier}, audience ${resource}${RESET}\n${DIM} valable ${minutes} min${accordes.length ? `, scopes : ${accordes.join(" ")}` : ", aucun scope"}${RESET}\n\n`);
297
+ if (nonAccordes.length > 0) w(`${DIM} ⚠️ non accordé(s) : ${nonAccordes.join(" ")} — réservé(s) au rôle d'administration, que le compte « ${identifier} » ne porte pas.${RESET}\n\n`);
298
+ if (write) {
299
+ const requested = requestedAgents(opts.agent);
300
+ if (requested instanceof Error) {
301
+ this.log(requested.message, "ERROR");
302
+ process.exitCode = 1;
303
+ return this;
304
+ }
305
+ let targets = requested ?? [];
306
+ if (requested === void 0) {
307
+ const presents = this.#agentsPresents();
308
+ const porteurs = presents.filter((c) => alreadyHasKey(c.forme, this.#contentOf(c), MCP_TOKEN_ENV));
309
+ const added = presents.filter((c) => !porteurs.includes(c));
310
+ targets = porteurs;
311
+ if (porteurs.length === 0 && added.length > 0 && process.stdin.isTTY) {
312
+ const { checkbox } = await chargePrompts();
313
+ const chosen = await checkbox({
314
+ message: "Poser le jeton chez quels agents ?",
315
+ choices: added.map((c) => ({
316
+ name: `${c.name} — ${c.scope === "projet" ? c.file : `$${c.home}/${c.file}`}`,
317
+ value: c.key,
318
+ checked: true
319
+ }))
320
+ });
321
+ targets = added.filter((c) => chosen.includes(c.key));
322
+ } else if (porteurs.length === 0) targets = added;
323
+ else if (added.length > 0) w(`${DIM} ${added.map((c) => c.name).join(", ")} ${added.length > 1 ? "sont présents" : "est présent"} mais ne porte${added.length > 1 ? "nt" : ""} pas encore le jeton — ajoute --agent ${added.map((c) => c.key).join(",")}.${RESET}\n\n`);
324
+ }
325
+ if (this.#writeForAgents(token, w, targets) === 0) w(`${YELLOW}⚠ aucun agent reconnu dans ce projet — rien n'est écrit.${RESET}\n${DIM} Les agents connus rangent leur configuration ici :${RESET}\n` + AGENT_TARGETS.map((c) => `${DIM} ${c.name} : ${c.scope === "projet" ? c.file : `$${c.home ?? "HOME"}/${c.file}`}${RESET}\n`).join("") + `\n Le geste qui vaut pour TOUS — dans le shell d'où tu lances l'agent :\n\n ${BOLD}export ${MCP_TOKEN_ENV}=${token}${RESET}\n\n`);
326
+ w(`${DIM} Vibe et Codex prennent le NOM de la variable, pas le secret — déclare la porte une fois :${RESET}\n\n ${DIM}vibe mcp add nodefony --transport streamable-http \\${RESET}\n ${DIM} --url ${resource} --api-key-env ${MCP_TOKEN_ENV}${RESET}\n ${DIM}codex mcp add nodefony --url ${resource} \\${RESET}\n ${DIM} --bearer-token-env-var ${MCP_TOKEN_ENV}${RESET}\n\n`);
327
+ return this;
328
+ }
329
+ w(` export ${MCP_TOKEN_ENV}=${token}\n\n`);
330
+ w(`${DIM} --write pose la valeur chez les agents présents · nodefony ai:mcp --auth câble .mcp.json${RESET}\n\n`);
331
+ return this;
332
+ }
333
+ };
334
+ //#endregion
335
+ export { SecurityToken as default, ttlSeconds };
@@ -0,0 +1,131 @@
1
+ import { Command, askPasswordMasked } from "nodefony";
2
+ //#region nodefony/command/security-user-add.ts
3
+ const options = {
4
+ helpGroup: "COMPTES ET SECRETS",
5
+ showBanner: false,
6
+ kernelEvent: "onPostReady"
7
+ };
8
+ const GREEN = "\x1B[32m";
9
+ const YELLOW = "\x1B[33m";
10
+ const DIM = "\x1B[2m";
11
+ const BOLD = "\x1B[1m";
12
+ const RESET = "\x1B[0m";
13
+ /** Rôles du raccourci `--admin` — À PLAT (ne dépend pas d'une hiérarchie). */
14
+ const ADMIN_ROLES = ["ROLE_ADMIN", "ROLE_NODEFONY_ADMIN"];
15
+ /** Rôle de base, toujours proposé même si l'application n'en déclare aucun. */
16
+ const ROLE_BASE = "ROLE_USER";
17
+ /**
18
+ * `nodefony security:user:add [identifier]` — crée un compte utilisateur via le
19
+ * service applicatif `users` (hash Argon2id fait par `UserService.createUser`,
20
+ * jamais de mot de passe stocké en clair).
21
+ *
22
+ * Identifiant : en argument, ou DEMANDÉ quand un terminal est là — la commande
23
+ * est proposée au menu, où personne ne peut taper d'argument. Hors terminal
24
+ * (CI, script), l'absence reste une erreur, avec la ligne exacte à taper.
25
+ *
26
+ * Mot de passe : `--password` (visible dans l'historique shell — accepté pour
27
+ * les scripts) ou PROMPT MASQUÉ en TTY (recommandé). Rôles : `--roles a,b,c`
28
+ * (CSV) ou `--admin` (raccourci `ROLE_ADMIN + ROLE_NODEFONY_ADMIN` → accès
29
+ * console Studio). Défaut : `ROLE_USER`.
30
+ *
31
+ * Le service `users` est posé par l'APPLICATION (cf `provisionUsers` du
32
+ * template d'app) — absent = message actionnable, pas de stack.
33
+ */
34
+ var SecurityUserAdd = class extends Command {
35
+ constructor(cli) {
36
+ super("security:user:add", "crée un compte (mot de passe demandé masqué)", cli, options);
37
+ this.addArgument("[identifier]", "identifiant (login) du compte");
38
+ this.addOption("-p, --password <password>", "mot de passe (sinon : prompt masqué en TTY)");
39
+ this.addOption("-r, --roles <roles>", "rôles CSV (ex: ROLE_USER,ROLE_DEV) — défaut ROLE_USER");
40
+ this.addOption("-a, --admin", `compte administrateur (${ADMIN_ROLES.join(" + ")} — accès Studio)`);
41
+ }
42
+ /**
43
+ * Rôles que CETTE application connaît, lus de sa hiérarchie déclarée.
44
+ *
45
+ * 🔴 Jamais une liste recopiée ici : une constante en dur diverge dès que
46
+ * l'application déclare un rôle métier, et la commande proposerait alors des
47
+ * rôles qui n'existent pas tout en taisant ceux qui existent. La hiérarchie
48
+ * (`security.roleHierarchy`) est la seule source — ses clés sont les rôles
49
+ * qui couvrent, ses valeurs ceux qui sont couverts.
50
+ *
51
+ * Un rôle inconnu du RBAC n'est pas une faute : il est simplement inerte.
52
+ * On ne refuse donc rien, on PROPOSE ce qu'on sait.
53
+ */
54
+ #knownRoles() {
55
+ const hierarchy = ((this.kernel?.modules)?.security?.options)?.roleHierarchy;
56
+ const all = /* @__PURE__ */ new Set([ROLE_BASE]);
57
+ for (const [porteur, couverts] of Object.entries(hierarchy ?? {})) {
58
+ all.add(porteur);
59
+ for (const c of couverts) all.add(c);
60
+ }
61
+ return [ROLE_BASE, ...[...all].filter((r) => r !== ROLE_BASE).sort()];
62
+ }
63
+ async generate(identifierArg, opts) {
64
+ let identifier;
65
+ try {
66
+ identifier = await this.askArgument(identifierArg, {
67
+ name: "identifier",
68
+ message: "Identifiant (login) du compte :"
69
+ });
70
+ } catch (e) {
71
+ this.log(e.message, "ERROR");
72
+ process.exitCode = 1;
73
+ return this;
74
+ }
75
+ const users = this.kernel?.container?.get("users");
76
+ if (!users) {
77
+ this.log("service \"users\" absent — l'application ne provisionne pas son annuaire utilisateurs. Ajoute-le (cf nodefony/security/provisionUsers.ts d'une app générée : container.set(\"users\", new UserService(repo, encoder)) à onKernelReady).", "ERROR");
78
+ process.exitCode = 1;
79
+ return this;
80
+ }
81
+ if (await users.findByIdentifier(identifier)) {
82
+ this.log(`le compte « ${identifier} » existe déjà.\n · le voir : nodefony security:user:list -q ${identifier}\n · le supprimer : nodefony security:user:delete ${identifier}\n · mot de passe oublié : par Studio (/nodefony) — la commande n'existe pas encore.`, "ERROR");
83
+ process.exitCode = 1;
84
+ return this;
85
+ }
86
+ let password = opts.password;
87
+ if (!password) {
88
+ if (!process.stdin.isTTY) {
89
+ this.log("mot de passe requis : --password <pwd> (pas de prompt hors terminal).", "ERROR");
90
+ process.exitCode = 1;
91
+ return this;
92
+ }
93
+ password = await askPasswordMasked(`${BOLD}Mot de passe de « ${identifier} »${RESET} ${DIM}(frappe masquée)${RESET} : `);
94
+ const confirmed = await askPasswordMasked(`${BOLD}Confirme le mot de passe${RESET} : `);
95
+ if (password !== confirmed) {
96
+ this.log("les deux saisies diffèrent — rien n'a été créé.", "ERROR");
97
+ process.exitCode = 1;
98
+ return this;
99
+ }
100
+ }
101
+ if (!password) {
102
+ this.log("mot de passe vide — rien n'a été créé.", "ERROR");
103
+ process.exitCode = 1;
104
+ return this;
105
+ }
106
+ let roles;
107
+ if (opts.admin) roles = ADMIN_ROLES;
108
+ else if (opts.roles) roles = opts.roles.split(",").map((r) => r.trim()).filter(Boolean);
109
+ else if (process.stdin.isTTY) {
110
+ await this.loadPrompts();
111
+ const chosen = await this.prompts.checkbox({
112
+ message: `Rôles de « ${identifier} » :`,
113
+ choices: this.#knownRoles().map((r) => ({
114
+ name: r,
115
+ value: r,
116
+ checked: r === ROLE_BASE
117
+ }))
118
+ });
119
+ roles = chosen.length > 0 ? chosen : [ROLE_BASE];
120
+ } else roles = [ROLE_BASE];
121
+ const user = await users.createUser({
122
+ identifier,
123
+ plainPassword: password,
124
+ roles
125
+ });
126
+ process.stdout.write(`\n${GREEN}✓ compte créé${RESET} — ${BOLD}${user.identifier}${RESET} ${DIM}(id ${user.id})${RESET}\n rôles : ${roles.join(" · ")}\n` + (opts.admin ? ` ${DIM}accès console Studio : /nodefony${RESET}\n` : "") + (opts.password ? ` ${YELLOW}⚠ mot de passe passé en argument — pense à purger l'historique shell${RESET}\n` : ""));
127
+ return this;
128
+ }
129
+ };
130
+ //#endregion
131
+ export { SecurityUserAdd as default };
@@ -0,0 +1,102 @@
1
+ import { Command } from "nodefony";
2
+ //#region nodefony/command/security-user-delete.ts
3
+ const options = {
4
+ helpGroup: "COMPTES ET SECRETS",
5
+ showBanner: false,
6
+ kernelEvent: "onPostReady",
7
+ quietBoot: true
8
+ };
9
+ const GREEN = "\x1B[32m";
10
+ const YELLOW = "\x1B[33m";
11
+ const DIM = "\x1B[2m";
12
+ const BOLD = "\x1B[1m";
13
+ const RESET = "\x1B[0m";
14
+ /**
15
+ * Rôle qui donne la main sur l'instance — celui dont il faut toujours garder un
16
+ * porteur actif, sinon plus personne ne peut administrer l'application.
17
+ */
18
+ const ADMIN_ROLE = "ROLE_NODEFONY_ADMIN";
19
+ /**
20
+ * `nodefony security:user:delete <identifiant>` — retire un compte.
21
+ *
22
+ * **Un geste destructeur ne se fait pas sur une frappe.** La commande MONTRE ce
23
+ * qu'elle va supprimer (identifiant, rôles, identifiant interne), puis demande
24
+ * une confirmation. Hors terminal, elle exige `--yes` : un script qui supprime
25
+ * doit le dire dans sa ligne, jamais l'obtenir d'un prompt sauté.
26
+ *
27
+ * **Garde-fou anti-lockout** : le DERNIER administrateur actif ne se supprime
28
+ * pas. Sans lui, plus personne n'accède à la console d'administration, et le
29
+ * seul recours est une écriture directe en base. Le même garde-fou existe côté
30
+ * data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
31
+ *
32
+ * La révocation des sessions et jetons du compte suit d'elle-même : la
33
+ * suppression émet `onUserRevoked`, auquel `@nodefony/security` réagit en
34
+ * éjectant sessions et jetons. Rien à faire de plus ici.
35
+ */
36
+ var SecurityUserDelete = class extends Command {
37
+ constructor(cli) {
38
+ super("security:user:delete", "supprime un compte, après confirmation", cli, options);
39
+ this.addArgument("[identifier]", "identifiant (login) du compte à retirer");
40
+ this.addOption("-y, --yes", "ne pas demander confirmation (obligatoire hors terminal)");
41
+ }
42
+ async generate(identifierArg, opts) {
43
+ const users = this.kernel?.container?.get("users");
44
+ if (!users) {
45
+ this.log("service « users » absent — l'application ne provisionne pas son annuaire utilisateurs.", "ERROR");
46
+ process.exitCode = 1;
47
+ return this;
48
+ }
49
+ let identifier;
50
+ try {
51
+ identifier = await this.askArgument(identifierArg, {
52
+ name: "identifier",
53
+ message: "Compte à SUPPRIMER :"
54
+ });
55
+ } catch (e) {
56
+ this.log(e.message, "ERROR");
57
+ process.exitCode = 1;
58
+ return this;
59
+ }
60
+ const user = await users.findByIdentifier(identifier);
61
+ if (!user) {
62
+ this.log(`aucun compte « ${identifier} » — nodefony security:user:list`, "ERROR");
63
+ process.exitCode = 1;
64
+ return this;
65
+ }
66
+ if ((user.roles ?? []).includes(ADMIN_ROLE)) {
67
+ if (await users.countActiveAdmins(ADMIN_ROLE) <= 1) {
68
+ this.log(`« ${identifier} » est le DERNIER administrateur actif — refus.\n Sans lui, plus personne n'administre cette application.\n Crée d'abord un autre admin : nodefony security:user:add <id> --admin`, "ERROR");
69
+ process.exitCode = 1;
70
+ return this;
71
+ }
72
+ }
73
+ const w = (t) => {
74
+ process.stdout.write(t);
75
+ };
76
+ w(`\n${BOLD}Compte à supprimer${RESET}\n identifiant : ${BOLD}${user.identifier}${RESET}\n rôles : ${(user.roles ?? []).join(", ") || "—"}\n id interne : ${DIM}${user.id}${RESET}\n\n`);
77
+ if (!opts.yes) {
78
+ if (!process.stdin.isTTY) {
79
+ this.log("confirmation impossible sans terminal — relance avec --yes si c'est voulu.", "ERROR");
80
+ process.exitCode = 1;
81
+ return this;
82
+ }
83
+ await this.loadPrompts();
84
+ if (!await this.prompts.confirm({
85
+ message: `Supprimer définitivement « ${identifier} » ?`,
86
+ default: false
87
+ })) {
88
+ w(`${YELLOW}annulé — rien n'a été supprimé.${RESET}\n\n`);
89
+ return this;
90
+ }
91
+ }
92
+ if (await users.delete({ id: user.id }) === 0) {
93
+ this.log(`aucune ligne supprimée pour « ${identifier} » — le compte a-t-il disparu entre-temps ?`, "ERROR");
94
+ process.exitCode = 1;
95
+ return this;
96
+ }
97
+ w(`${GREEN}✓ compte supprimé${RESET} — ${BOLD}${identifier}${RESET}\n${DIM} ses sessions et ses jetons sont révoqués (event onUserRevoked).${RESET}\n\n`);
98
+ return this;
99
+ }
100
+ };
101
+ //#endregion
102
+ export { SecurityUserDelete as default };
@@ -0,0 +1,77 @@
1
+ import { Command } from "nodefony";
2
+ //#region nodefony/command/security-user-list.ts
3
+ const options = {
4
+ helpGroup: "COMPTES ET SECRETS",
5
+ showBanner: false,
6
+ kernelEvent: "onReady",
7
+ quietBoot: true
8
+ };
9
+ const DIM = "\x1B[2m";
10
+ const BOLD = "\x1B[1m";
11
+ const RESET = "\x1B[0m";
12
+ /** Défaut volontairement BORNÉ — une liste se borne, toujours. */
13
+ const DEFAULT_LIMIT = 50;
14
+ /**
15
+ * `nodefony security:user:list` — qui a un compte dans cette application.
16
+ *
17
+ * **Lister n'est pas une fuite, et c'était la question.** Cette commande
18
+ * s'exécute sur la machine qui possède déjà la base : elle ne divulgue rien que
19
+ * son utilisateur ne puisse lire avec un client SQL. Ce qui serait dangereux est
20
+ * ailleurs, et gardé ici : **jamais le hachage du mot de passe**, jamais les
21
+ * secrets de second facteur, jamais les jetons d'un fournisseur social. La
22
+ * sortie est composée champ par champ — un `console.table(user)` livrerait le
23
+ * credential, parce que le repository, lui, le voit.
24
+ *
25
+ * Le contraire — ne pas pouvoir lister — a un coût réel : on crée un compte, on
26
+ * ne voit pas ce qu'on a créé, et on recommence en doublon.
27
+ *
28
+ * La pagination est NATIVE (`users.listPage`) : jamais un `find()` complet
29
+ * ramené en mémoire, indolore sur trois comptes et fatal sur cent mille.
30
+ */
31
+ var SecurityUserList = class extends Command {
32
+ constructor(cli) {
33
+ super("security:user:list", "liste les comptes : identifiant, rôles, état", cli, options);
34
+ this.addOption("-q, --query <texte>", "filtre sur l'identifiant (sous-chaîne, insensible à la casse)");
35
+ this.addOption("-r, --role <role>", "n'affiche que les porteurs d'un rôle");
36
+ this.addOption("-l, --limit <n>", `nombre maximum de comptes (défaut ${DEFAULT_LIMIT})`);
37
+ this.addOption("-j, --json", "sortie JSON (scripts/CI)");
38
+ }
39
+ async generate(opts) {
40
+ const users = this.kernel?.container?.get("users");
41
+ if (!users) {
42
+ this.log("service « users » absent — l'application ne provisionne pas son annuaire utilisateurs.", "ERROR");
43
+ process.exitCode = 1;
44
+ return this;
45
+ }
46
+ const limit = Number.parseInt(opts.limit ?? "", 10);
47
+ const page = await users.listPage({
48
+ limit: Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_LIMIT,
49
+ ...opts.query ? { q: opts.query } : {},
50
+ ...opts.role ? { role: opts.role } : {}
51
+ });
52
+ const rows = page.items.map((u) => ({
53
+ identifiant: u.identifier,
54
+ rôles: (u.roles ?? []).join(", ") || "—",
55
+ actif: u.isActive() ? "oui" : "non",
56
+ verrouillé: u.isLocked() ? "OUI" : "—",
57
+ id: u.id
58
+ }));
59
+ if (opts.json) {
60
+ process.stdout.write(`${JSON.stringify({
61
+ items: rows,
62
+ hasNext: page.hasNext
63
+ }, null, 2)}\n`);
64
+ return this;
65
+ }
66
+ if (rows.length === 0) {
67
+ process.stdout.write(`aucun compte${opts.query || opts.role ? " pour ce filtre" : ""} — nodefony security:user:add <identifiant>\n`);
68
+ return this;
69
+ }
70
+ process.stdout.write(`\n${BOLD}👤 Comptes${RESET}\n`);
71
+ console.table(rows);
72
+ process.stdout.write(`${rows.length} compte(s)` + (page.hasNext ? `${DIM} — page bornée, il y en a d'autres (--limit)${RESET}` : "") + `\n\n`);
73
+ return this;
74
+ }
75
+ };
76
+ //#endregion
77
+ export { SecurityUserList as default };