@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,56 @@
1
+ //#region nodefony/src/authenticator/peekIssuer.ts
2
+ /**
3
+ * Lecture NON VÉRIFIÉE de l'émetteur d'un JWS compact.
4
+ *
5
+ * Sert à **choisir qui doit examiner un jeton**, jamais à décider de son sort.
6
+ * Deux authenticators reconnaissent la même forme de credential — un
7
+ * `Authorization: Bearer <jws>` — l'un pour les jetons que Nodefony a émis,
8
+ * l'autre pour ceux d'un serveur d'autorisation tiers. Sans discriminant, le
9
+ * premier listé dans la zone capture les deux et refuse la moitié des jetons :
10
+ * l'ordre de la configuration deviendrait une décision de sécurité, et son
11
+ * erreur ne se verrait qu'en production.
12
+ *
13
+ * ## Ce qui rend cette lecture sûre
14
+ *
15
+ * Rien de ce qui est lu ici ne devient une clé, une URL ou un algorithme. La
16
+ * valeur ne sert qu'à sélectionner une entrée dans une liste **fermée**, écrite
17
+ * en configuration ; le jeton est ensuite vérifié entièrement par
18
+ * l'authenticator retenu, `iss` compris. Un attaquant qui ment sur `iss` ne
19
+ * gagne donc que le droit d'être refusé par un autre maillon.
20
+ *
21
+ * La taille est bornée AVANT tout travail : `JSON.parse` sur une entrée non
22
+ * fiable est le genre d'appel qu'on ne laisse pas grandir sans limite.
23
+ */
24
+ /**
25
+ * Plus grand jeton qu'on accepte de regarder (octets).
26
+ *
27
+ * Un jeton d'accès réaliste tient largement en deçà, y compris avec des rôles
28
+ * et des groupes ; au-delà, refuser de décoder coûte moins cher que de
29
+ * décoder pour refuser.
30
+ */
31
+ const MAX_TOKEN_LENGTH = 8192;
32
+ /**
33
+ * Rend le claim `iss` d'un JWS compact, sans vérifier quoi que ce soit.
34
+ *
35
+ * @param raw - le jeton brut, tel que présenté
36
+ * @returns l'émetteur revendiqué, ou `null` si le jeton est trop gros, mal
37
+ * formé, ou ne revendique pas d'émetteur exploitable
38
+ */
39
+ function peekIssuer(raw) {
40
+ if (raw.length === 0 || raw.length > MAX_TOKEN_LENGTH) return null;
41
+ const first = raw.indexOf(".");
42
+ if (first <= 0) return null;
43
+ const second = raw.indexOf(".", first + 1);
44
+ if (second <= first + 1) return null;
45
+ try {
46
+ const json = Buffer.from(raw.slice(first + 1, second), "base64url").toString("utf8");
47
+ const claims = JSON.parse(json);
48
+ if (typeof claims !== "object" || claims === null) return null;
49
+ const iss = claims.iss;
50
+ return typeof iss === "string" && iss.length > 0 ? iss : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ //#endregion
56
+ export { peekIssuer as default, peekIssuer };
@@ -0,0 +1,79 @@
1
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+ //#region nodefony/src/crypto/secretCipher.ts
4
+ /**
5
+ * Chiffrement **réversible** de secrets applicatifs au repos — AES-256-GCM
6
+ * (authentifié). Brique générique partagée par tous les domaines qui doivent
7
+ * **relire** un secret en clair (≠ mots de passe, hachés à sens unique) : TOTP
8
+ * (clé `K` relue à chaque vérification), webhooks (secret de signature relu à
9
+ * chaque livraison), etc.
10
+ *
11
+ * GCM apporte confidentialité ET intégrité : un blob altéré, ou déchiffré avec
12
+ * la mauvaise clé, fait échouer `final()` (tag invalide) → toute manipulation du
13
+ * secret stocké est détectée.
14
+ *
15
+ * **Séparation de domaine cryptographique** : chaque domaine dérive sa clé via
16
+ * {@link deriveKey} avec un `info`/`salt` HKDF distinct. Une même clé maître de
17
+ * config produit ainsi des sous-clés **indépendantes** par domaine (un blob
18
+ * webhook ne se déchiffre jamais avec la clé TOTP, par construction).
19
+ *
20
+ * Format du blob : `gcm1.<base64url(iv ‖ tag ‖ ciphertext)>` — préfixe versionné
21
+ * pour permettre une rotation d'algorithme future sans casser les secrets existants.
22
+ */
23
+ const VERSION = "gcm1";
24
+ const IV_BYTES = 12;
25
+ const KEY_BYTES = 32;
26
+ /**
27
+ * Dérive une clé AES-256 (32 octets) d'un matériel de clé applicatif via
28
+ * HKDF-SHA256 (RFC 5869). Accepte toute longueur/forme (passphrase, hex, base64)
29
+ * sans jamais l'utiliser brute comme clé AES. **Déterministe** : tous les pods
30
+ * d'un cluster dérivent la même clé du même secret de config + même domaine
31
+ * (secret lisible cross-pod). Domaines distincts (`info` différent) → clés
32
+ * indépendantes.
33
+ *
34
+ * @param material - matériel de clé brut (secret de config).
35
+ * @param derivation - sel + info propres au domaine (TOTP, webhook…).
36
+ * @returns clé AES-256 (32 octets).
37
+ */
38
+ function deriveKey(material, derivation) {
39
+ const ikm = typeof material === "string" ? Buffer.from(material, "utf8") : material;
40
+ const salt = typeof derivation.salt === "string" ? Buffer.from(derivation.salt) : derivation.salt;
41
+ const info = typeof derivation.info === "string" ? Buffer.from(derivation.info) : derivation.info;
42
+ return Buffer.from(hkdfSync("sha256", ikm, salt, info, KEY_BYTES));
43
+ }
44
+ /** Génère une clé éphémère 32 octets (dev sans clé configurée — non persistée). */
45
+ function generateEphemeralKey() {
46
+ return randomBytes(KEY_BYTES);
47
+ }
48
+ /** Chiffre un secret en clair → blob opaque versionné (IV aléatoire à chaque appel). */
49
+ function encryptSecret(plain, key) {
50
+ const iv = randomBytes(IV_BYTES);
51
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
52
+ const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
53
+ const tag = cipher.getAuthTag();
54
+ return `${VERSION}.${Buffer.concat([
55
+ iv,
56
+ tag,
57
+ ct
58
+ ]).toString("base64url")}`;
59
+ }
60
+ /**
61
+ * Déchiffre un blob produit par {@link encryptSecret}. Lève si le format/version
62
+ * est invalide, le blob tronqué, ou le tag GCM non valide (altération OU mauvaise
63
+ * clé — GCM ne distingue pas les deux, par construction).
64
+ */
65
+ function decryptSecret(blob, key) {
66
+ const dot = blob.indexOf(".");
67
+ const version = dot === -1 ? "" : blob.slice(0, dot);
68
+ if (version !== VERSION) throw new Error(`secret cipher: format/version invalide (« ${version} »)`);
69
+ const raw = Buffer.from(blob.slice(dot + 1), "base64url");
70
+ if (raw.length < 28) throw new Error("secret cipher: blob tronqué");
71
+ const iv = raw.subarray(0, IV_BYTES);
72
+ const tag = raw.subarray(IV_BYTES, 28);
73
+ const ct = raw.subarray(28);
74
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
75
+ decipher.setAuthTag(tag);
76
+ return Buffer.concat([decipher.update(ct), decipher.final()]);
77
+ }
78
+ //#endregion
79
+ export { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey };
@@ -0,0 +1,54 @@
1
+ //#region nodefony/src/csp.ts
2
+ /**
3
+ * Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
4
+ * directives et des sources est préservé (déterminisme : header stable, tests
5
+ * fiables). Les séparateurs multiples / espaces superflus sont normalisés.
6
+ */
7
+ function parseCsp(csp) {
8
+ const out = [];
9
+ for (const part of csp.split(";")) {
10
+ const tokens = part.trim().split(/\s+/).filter(Boolean);
11
+ if (tokens.length === 0) continue;
12
+ const name = tokens[0];
13
+ out.push([name, tokens.slice(1)]);
14
+ }
15
+ return out;
16
+ }
17
+ /** Sérialise des directives ordonnées en chaîne CSP (`a b; c d`). */
18
+ function serializeCsp(directives) {
19
+ return directives.map(([name, src]) => src.length ? `${name} ${src.join(" ")}` : name).join("; ");
20
+ }
21
+ /**
22
+ * Fusionne un CSP de base avec des fragments additifs par module.
23
+ *
24
+ * - directive déjà dans la base → ses sources sont COMPLÉTÉES (dédupliquées,
25
+ * ordre base d'abord puis ajouts) ;
26
+ * - directive absente → AJOUTÉE en fin (ordre d'apparition des fragments).
27
+ *
28
+ * Pur + déterministe : recalculé uniquement quand un module (dé)enregistre ses
29
+ * origines (jamais par requête). Le résultat repart dans `SecurityHeaders`, qui
30
+ * re-split autour de `{{nonce}}` au boot → 1 `join` par requête (hot-path inchangé).
31
+ *
32
+ * @param base - CSP de configuration (peut contenir `'nonce-{{nonce}}'`).
33
+ * @param fragments - fragments additifs (un par module enregistré).
34
+ * @returns la chaîne CSP fusionnée.
35
+ */
36
+ function mergeCspFragments(base, fragments) {
37
+ const directives = parseCsp(base);
38
+ const index = /* @__PURE__ */ new Map();
39
+ for (const [name, src] of directives) index.set(name, src);
40
+ for (const fragment of fragments) for (const name in fragment) {
41
+ const additions = fragment[name];
42
+ if (additions.length === 0) continue;
43
+ let sources = index.get(name);
44
+ if (!sources) {
45
+ sources = [];
46
+ index.set(name, sources);
47
+ directives.push([name, sources]);
48
+ }
49
+ for (const src of additions) if (!sources.includes(src)) sources.push(src);
50
+ }
51
+ return serializeCsp(directives);
52
+ }
53
+ //#endregion
54
+ export { mergeCspFragments, parseCsp, serializeCsp };
@@ -0,0 +1,65 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ //#region nodefony/src/csrfToken.ts
3
+ /**
4
+ * Jeton synchronizer CSRF — modèle **double-submit signé** (OWASP CSRF Prevention
5
+ * Cheat Sheet, « Signed Double-Submit Cookie »). Le token est
6
+ * `nonce.HMAC-SHA256(secret, nonce)` (base64url), posé dans un cookie LISIBLE
7
+ * (`csrf-token`, non HttpOnly) ET rejoué par le client dans l'en-tête
8
+ * `x-csrf-token`. La défense `@CsrfProtect` exige les deux PRÉSENTS, ÉGAUX
9
+ * (double-submit) et la signature HMAC VALIDE.
10
+ *
11
+ * **Stateless** (aucune session requise) → couvre le BFF (cookie de session) ET
12
+ * l'API JWT sans coupler au stockage de session. Le secret HMAC empêche un script
13
+ * tiers de forger un token (il ne peut pas calculer la signature) ; le double
14
+ * submit empêche un attaquant cross-site d'en injecter un (il ne peut ni écrire
15
+ * l'en-tête custom — préflight CORS — ni lire le cookie de la victime — SameSite + SOP).
16
+ *
17
+ * Pure et synchrone (1 HMAC à l'émission, 1 à la vérif) — payé UNIQUEMENT sur les
18
+ * routes `@CsrfProtect` (la défense globale Fetch Metadata reste primaire, hot-path
19
+ * GET = 0).
20
+ *
21
+ * @see OWASP CSRF Prevention Cheat Sheet · RFC 9110 §15.5.4 (403).
22
+ */
23
+ var CsrfTokenManager = class CsrfTokenManager {
24
+ #secret;
25
+ /** Octets aléatoires du nonce (144 bits) — imprévisible, compact en base64url. */
26
+ static #NONCE_BYTES = 18;
27
+ constructor(secret) {
28
+ this.#secret = Buffer.from(secret, "utf8");
29
+ }
30
+ /** Émet un token signé `nonce.signature` (base64url). */
31
+ issue() {
32
+ const nonce = randomBytes(CsrfTokenManager.#NONCE_BYTES).toString("base64url");
33
+ return `${nonce}.${this.#sign(nonce)}`;
34
+ }
35
+ /**
36
+ * Vérifie une mutation `@CsrfProtect` : en-tête ET cookie présents, ÉGAUX
37
+ * (double-submit, comparaison à temps constant) et signature HMAC valide. Tout
38
+ * écart → `false` (le firewall lève alors un 403). Jamais d'exception.
39
+ *
40
+ * @param headerToken - valeur de l'en-tête `x-csrf-token` rejouée par le client.
41
+ * @param cookieToken - valeur du cookie `csrf-token` posé par le serveur.
42
+ */
43
+ verify(headerToken, cookieToken) {
44
+ if (!headerToken || !cookieToken) return false;
45
+ if (!CsrfTokenManager.#timingSafeEqual(headerToken, cookieToken)) return false;
46
+ const dot = headerToken.lastIndexOf(".");
47
+ if (dot < 1 || dot === headerToken.length - 1) return false;
48
+ const nonce = headerToken.slice(0, dot);
49
+ const signature = headerToken.slice(dot + 1);
50
+ return CsrfTokenManager.#timingSafeEqual(signature, this.#sign(nonce));
51
+ }
52
+ /** HMAC-SHA256 du nonce, base64url. */
53
+ #sign(nonce) {
54
+ return createHmac("sha256", this.#secret).update(nonce).digest("base64url");
55
+ }
56
+ /** Comparaison à temps constant (longueurs différentes → false sans fuite de timing). */
57
+ static #timingSafeEqual(a, b) {
58
+ const ba = Buffer.from(a, "utf8");
59
+ const bb = Buffer.from(b, "utf8");
60
+ if (ba.length !== bb.length) return false;
61
+ return timingSafeEqual(ba, bb);
62
+ }
63
+ };
64
+ //#endregion
65
+ export { CsrfTokenManager, CsrfTokenManager as default };
@@ -0,0 +1,130 @@
1
+ import { SsrfError } from "../../errors/SsrfError.js";
2
+ import { BlockList, isIP } from "node:net";
3
+ import { lookup } from "node:dns/promises";
4
+ //#region nodefony/src/net/ssrfGuard.ts
5
+ /**
6
+ * Protection **SSRF** (Server-Side Request Forgery) générique pour les URL
7
+ * sortantes — endpoints webhook, fetch applicatif, futurs agents/RAG.
8
+ *
9
+ * Le danger : une URL fournie par un utilisateur peut viser une ressource
10
+ * **interne** non destinée à être exposée (loopback `127.0.0.1`, réseau privé
11
+ * `10.x`/`192.168.x`, et surtout l'endpoint de **métadonnées cloud**
12
+ * `169.254.169.254` qui livre les credentials IAM d'une VM). Le serveur, lui,
13
+ * peut atteindre ces cibles → il devient un proxy de l'attaquant.
14
+ *
15
+ * La défense résout le nom DNS **avant** toute connexion et rejette si une seule
16
+ * des IP résolues est non publique. Les adresses résolues sont retournées pour
17
+ * permettre un **pinning** à la connexion (anti DNS-rebinding : se connecter à
18
+ * l'IP validée, pas re-résoudre).
19
+ */
20
+ /** Plages IPv4 non routables sur l'Internet public (à bloquer). */
21
+ const BLOCKED_V4 = [
22
+ ["0.0.0.0", 8],
23
+ ["10.0.0.0", 8],
24
+ ["100.64.0.0", 10],
25
+ ["127.0.0.0", 8],
26
+ ["169.254.0.0", 16],
27
+ ["172.16.0.0", 12],
28
+ ["192.0.0.0", 24],
29
+ ["192.0.2.0", 24],
30
+ ["192.88.99.0", 24],
31
+ ["192.168.0.0", 16],
32
+ ["198.18.0.0", 15],
33
+ ["198.51.100.0", 24],
34
+ ["203.0.113.0", 24],
35
+ ["224.0.0.0", 4],
36
+ ["240.0.0.0", 4]
37
+ ];
38
+ /** Plages IPv6 non routables sur l'Internet public (à bloquer). */
39
+ const BLOCKED_V6 = [
40
+ ["::", 128],
41
+ ["::1", 128],
42
+ ["64:ff9b::", 96],
43
+ ["2002::", 16],
44
+ ["100::", 64],
45
+ ["2001:db8::", 32],
46
+ ["fc00::", 7],
47
+ ["fe80::", 10],
48
+ ["ff00::", 8]
49
+ ];
50
+ let blockList = null;
51
+ /** Construit (lazy, une fois) la liste de blocage des IP non publiques. */
52
+ function getBlockList() {
53
+ if (blockList === null) {
54
+ const bl = new BlockList();
55
+ for (const [net, prefix] of BLOCKED_V4) bl.addSubnet(net, prefix, "ipv4");
56
+ for (const [net, prefix] of BLOCKED_V6) bl.addSubnet(net, prefix, "ipv6");
57
+ blockList = bl;
58
+ }
59
+ return blockList;
60
+ }
61
+ /**
62
+ * Indique si une IP (littérale) est **non publique** (donc interdite comme cible
63
+ * sortante). Une IP syntaxiquement invalide est considérée bloquée (fail-closed).
64
+ *
65
+ * `node:net` `BlockList` rabat **nativement** les IPv6 **IPv4-mapped** — toutes
66
+ * notations confondues (`::ffff:127.0.0.1`, `::ffff:7f00:1`,
67
+ * `0:0:0:0:0:ffff:127.0.0.1`…) — sur les règles IPv4 : `::ffff:169.254.169.254`
68
+ * est bloqué sans traitement spécial (vérifié red-team `webhookSsrf.attack`).
69
+ * ⚠️ NE PAS ajouter `::ffff:0:0/96` à la liste : Node ferait alors matcher TOUTE
70
+ * adresse IPv4 (faux positif massif → 100 % des webhooks rejetés).
71
+ *
72
+ * @param ip - adresse IPv4 ou IPv6 (sans crochets).
73
+ */
74
+ function isBlockedAddress(ip) {
75
+ const family = isIP(ip);
76
+ if (family === 0) return true;
77
+ return getBlockList().check(ip, family === 4 ? "ipv4" : "ipv6");
78
+ }
79
+ const defaultResolver = async (hostname) => {
80
+ return (await lookup(hostname, { all: true })).map((r) => r.address);
81
+ };
82
+ /** Retire les crochets d'un hostname IPv6 littéral (`[::1]` → `::1`). */
83
+ function stripBrackets(hostname) {
84
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
85
+ }
86
+ /**
87
+ * Valide qu'une URL sortante est sûre (anti-SSRF) : protocole autorisé, pas
88
+ * d'identifiants embarqués, hôte résolvable, et **toutes** les IP résolues
89
+ * publiques. Lève {@link SsrfError} (422) sinon.
90
+ *
91
+ * @param rawUrl - URL brute à valider.
92
+ * @param options - politique (https-only, deny private, résolveur).
93
+ * @returns l'URL normalisée + les IP résolues (pour pinning à la connexion).
94
+ * @throws SsrfError si l'URL est malformée ou cible une ressource interdite.
95
+ */
96
+ async function assertPublicUrl(rawUrl, options = {}) {
97
+ let url;
98
+ try {
99
+ url = new URL(rawUrl);
100
+ } catch {
101
+ throw new SsrfError(`URL invalide : « ${rawUrl} »`);
102
+ }
103
+ const scheme = options.allowHttp ? ["https:", "http:"] : ["https:"];
104
+ if (!scheme.includes(url.protocol)) throw new SsrfError(`protocole « ${url.protocol} » interdit (attendu : ${scheme.join("/")})`);
105
+ if (url.username !== "" || url.password !== "") throw new SsrfError("identifiants embarqués dans l'URL interdits");
106
+ const host = stripBrackets(url.hostname);
107
+ if (host === "") throw new SsrfError("hôte absent");
108
+ if (options.allowPrivate) return {
109
+ url,
110
+ addresses: []
111
+ };
112
+ let addresses;
113
+ if (isIP(host) !== 0) addresses = [host];
114
+ else {
115
+ const resolver = options.resolver ?? defaultResolver;
116
+ try {
117
+ addresses = await resolver(host);
118
+ } catch {
119
+ throw new SsrfError(`hôte non résolvable : « ${host} »`);
120
+ }
121
+ if (addresses.length === 0) throw new SsrfError(`hôte sans adresse : « ${host} »`);
122
+ }
123
+ for (const ip of addresses) if (isBlockedAddress(ip)) throw new SsrfError(`cible non publique interdite : ${host} → ${ip}`);
124
+ return {
125
+ url,
126
+ addresses
127
+ };
128
+ }
129
+ //#endregion
130
+ export { assertPublicUrl, isBlockedAddress };
@@ -0,0 +1,37 @@
1
+ import { createOidcProvider } from "./providers/oidc.js";
2
+ import { createGithubProvider } from "./providers/github.js";
3
+ //#region nodefony/src/oauth/oauthProviderRegistry.ts
4
+ const factories = /* @__PURE__ */ new Map();
5
+ /**
6
+ * Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
7
+ * builtins au chargement, et par une application pour ses fournisseurs.
8
+ */
9
+ function registerOAuthProvider(name, factory) {
10
+ factories.set(name, factory);
11
+ }
12
+ /** Fabrique d'un fournisseur par nom, ou `undefined` si inconnu. */
13
+ function getOAuthProviderFactory(name) {
14
+ return factories.get(name);
15
+ }
16
+ /** Noms enregistrés (validation boot, introspection Studio, tests). */
17
+ function listOAuthProviders() {
18
+ return [...factories.keys()];
19
+ }
20
+ registerOAuthProvider("google", (ctx) => createOidcProvider({
21
+ name: "google",
22
+ client: new ctx.arctic.Google(ctx.clientId, ctx.clientSecret, ctx.redirectUri),
23
+ issuer: "https://accounts.google.com",
24
+ decodeIdToken: ctx.arctic.decodeIdToken
25
+ }));
26
+ registerOAuthProvider("keycloak", (ctx) => {
27
+ if (!ctx.issuer) throw new Error("OAuth provider \"keycloak\" : config \"issuer\" requise (URL du realm, ex. https://kc.example/realms/app).");
28
+ return createOidcProvider({
29
+ name: "keycloak",
30
+ client: new ctx.arctic.KeyCloak(ctx.issuer, ctx.clientId, ctx.clientSecret, ctx.redirectUri),
31
+ issuer: ctx.issuer,
32
+ decodeIdToken: ctx.arctic.decodeIdToken
33
+ });
34
+ });
35
+ registerOAuthProvider("github", createGithubProvider);
36
+ //#endregion
37
+ export { getOAuthProviderFactory, listOAuthProviders, registerOAuthProvider };
@@ -0,0 +1,65 @@
1
+ //#region nodefony/src/oauth/providers/github.ts
2
+ /** Scopes minimaux : profil public + emails (l'email primaire peut être privé). */
3
+ const DEFAULT_SCOPES = ["read:user", "user:email"];
4
+ const API = "https://api.github.com";
5
+ function ghHeaders(accessToken) {
6
+ return {
7
+ Authorization: `Bearer ${accessToken}`,
8
+ Accept: "application/vnd.github+json",
9
+ "User-Agent": "nodefony",
10
+ "X-GitHub-Api-Version": "2022-11-28"
11
+ };
12
+ }
13
+ async function ghGet(url, accessToken) {
14
+ const res = await fetch(url, { headers: ghHeaders(accessToken) });
15
+ if (!res.ok) throw new Error(`GitHub API ${url} → ${res.status}`);
16
+ return res.json();
17
+ }
18
+ /**
19
+ * Fournisseur **GitHub** (OAuth 2.0 simple, NON-OIDC). Pas de PKCE, pas d'ID
20
+ * token : le profil est lu via l'API REST (`/user`), et l'email — souvent privé —
21
+ * via `/user/emails` (scope `user:email`). GitHub n'émet pas de paramètre `iss`
22
+ * (`expectedIssuer = null`) : la défense anti-CSRF repose sur le `state`.
23
+ */
24
+ function createGithubProvider(ctx) {
25
+ const client = new ctx.arctic.GitHub(ctx.clientId, ctx.clientSecret, ctx.redirectUri);
26
+ return {
27
+ usesPkce: false,
28
+ expectedIssuer: null,
29
+ defaultScopes: DEFAULT_SCOPES,
30
+ createAuthorizationURL(state, _codeVerifier, scopes) {
31
+ return client.createAuthorizationURL(state, scopes);
32
+ },
33
+ validateAuthorizationCode(code, _codeVerifier) {
34
+ return client.validateAuthorizationCode(code);
35
+ },
36
+ async fetchProfile(tokens) {
37
+ const accessToken = tokens.accessToken();
38
+ const user = await ghGet(`${API}/user`, accessToken);
39
+ const id = user.id;
40
+ if (typeof id !== "number" && typeof id !== "string") throw new Error("GitHub /user sans identifiant.");
41
+ const login = typeof user.login === "string" ? user.login : null;
42
+ const name = typeof user.name === "string" ? user.name : login;
43
+ let email = typeof user.email === "string" ? user.email : null;
44
+ let emailVerified = email !== null;
45
+ if (email === null) {
46
+ const list = await ghGet(`${API}/user/emails`, accessToken);
47
+ const primary = list.find((e) => e.primary === true) ?? list[0] ?? null;
48
+ if (primary && typeof primary.email === "string") {
49
+ email = primary.email;
50
+ emailVerified = primary.verified === true;
51
+ }
52
+ }
53
+ return {
54
+ provider: "github",
55
+ providerId: String(id),
56
+ email,
57
+ emailVerified,
58
+ name,
59
+ raw: user
60
+ };
61
+ }
62
+ };
63
+ }
64
+ //#endregion
65
+ export { createGithubProvider };
@@ -0,0 +1,48 @@
1
+ //#region nodefony/src/oauth/providers/oidc.ts
2
+ const DEFAULT_OIDC_SCOPES = [
3
+ "openid",
4
+ "profile",
5
+ "email"
6
+ ];
7
+ /**
8
+ * Fabrique un {@link IOAuthProvider} **générique OIDC** — couvre TOUT fournisseur
9
+ * OpenID Connect sans code spécifique : le profil se lit toujours pareil (claims
10
+ * standard `sub`/`email`/`email_verified`/`name` de l'ID token). Ajouter un
11
+ * fournisseur OIDC = une entrée de quelques lignes (nom + classe arctic + issuer),
12
+ * pas un fichier.
13
+ *
14
+ * PKCE S256 systématique (RFC 7636) ; le profil vient de l'ID token signé obtenu
15
+ * du token endpoint via TLS (décodage suffisant en code flow, RFC 8725).
16
+ */
17
+ function createOidcProvider(opts) {
18
+ const requireVerifier = (codeVerifier) => {
19
+ if (codeVerifier === null) throw new Error(`${opts.name}: code_verifier requis (PKCE OIDC).`);
20
+ return codeVerifier;
21
+ };
22
+ return {
23
+ usesPkce: true,
24
+ expectedIssuer: opts.issuer,
25
+ defaultScopes: opts.defaultScopes ?? DEFAULT_OIDC_SCOPES,
26
+ createAuthorizationURL(state, codeVerifier, scopes) {
27
+ return opts.client.createAuthorizationURL(state, requireVerifier(codeVerifier), scopes);
28
+ },
29
+ validateAuthorizationCode(code, codeVerifier) {
30
+ return opts.client.validateAuthorizationCode(code, requireVerifier(codeVerifier));
31
+ },
32
+ async fetchProfile(tokens) {
33
+ const claims = opts.decodeIdToken(tokens.idToken());
34
+ const sub = claims.sub;
35
+ if (typeof sub !== "string" || sub.length === 0) throw new Error(`${opts.name}: ID token sans claim 'sub'.`);
36
+ return {
37
+ provider: opts.name,
38
+ providerId: sub,
39
+ email: typeof claims.email === "string" ? claims.email : null,
40
+ emailVerified: claims.email_verified === true,
41
+ name: typeof claims.name === "string" ? claims.name : null,
42
+ raw: claims
43
+ };
44
+ }
45
+ };
46
+ }
47
+ //#endregion
48
+ export { createOidcProvider };
@@ -0,0 +1,94 @@
1
+ //#region nodefony/src/realtime/UserRealtimeToken.ts
2
+ /** Aucun scope — figé et partagé (0 allocation pour le cas humain, le plus courant). */
3
+ const EMPTY_SCOPES = Object.freeze([]);
4
+ /**
5
+ * Adaptateur `IUser` → `IRealtimeToken` (jeton realtime).
6
+ *
7
+ * Construit UNIQUEMENT pour un utilisateur **authentifié** (le
8
+ * {@link FirewallRealtimeAuthenticator} ne l'instancie qu'après avoir vérifié
9
+ * l'identité résolue par le firewall) → `isAuthenticated()` est toujours `true`.
10
+ * Un visiteur non authentifié reste sur `ANONYMOUS_REALTIME_TOKEN` (posé par le
11
+ * hub realtime), jamais ici.
12
+ *
13
+ * Les rôles ET les scopes sont des **copies** (pas de fuite de la structure
14
+ * interne de l'utilisateur ni du jeton d'origine). Les deux axes voyagent :
15
+ * les rôles disent qui l'on est, les scopes ce qu'une clé déléguée a le droit
16
+ * de faire — et un agent qui perd ses scopes en passant sur la socket perd la
17
+ * garantie même du mode machine.
18
+ */
19
+ var UserRealtimeToken = class {
20
+ /**
21
+ * Mode d'authentification RÉEL de l'identité (`session`, `jwt`, `apikey`…),
22
+ * repris du jeton que le firewall a posé dans l'ALS.
23
+ *
24
+ * Il était jadis figé à `"session"`, ce qui faisait passer un agent authentifié
25
+ * par JWT pour un utilisateur à cookie — un mensonge sur l'identité, et la
26
+ * racine du bug qui révoquait les sockets machine à machine.
27
+ */
28
+ type;
29
+ #user;
30
+ /**
31
+ * Fonction de re-validation Zero Trust, fournie par le
32
+ * {@link FirewallRealtimeAuthenticator} qui seul connaît le mode
33
+ * d'authentification et ce qu'il faut relire (session BFF, ou bornes et
34
+ * denylist d'un jeton porteur) — le token reste découplé : juste une closure.
35
+ * `null` = aucune re-validation câblée (best-effort).
36
+ */
37
+ #revalidate;
38
+ /**
39
+ * Scopes délégués repris du jeton du firewall (clé API / JWT / OAuth). Vide
40
+ * pour une identité humaine, qui n'est pas downscopée.
41
+ */
42
+ #scopes;
43
+ /**
44
+ * @param user - identité authentifiée résolue par le firewall.
45
+ * @param revalidate - preuve de vie de l'identité, adaptée à son mode.
46
+ * @param type - mode d'authentification réel (défaut `"session"`).
47
+ * @param scopes - scopes délégués du jeton d'origine (défaut aucun).
48
+ */
49
+ constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES) {
50
+ this.#user = user;
51
+ this.#revalidate = revalidate;
52
+ this.type = type;
53
+ this.#scopes = scopes;
54
+ }
55
+ /**
56
+ * Zero Trust : l'identité qui a ouvert cette socket est-elle TOUJOURS vivante ?
57
+ *
58
+ * Selon le mode : une session BFF encore ouverte et toujours celle de ce compte
59
+ * (détecte la déconnexion et le changement de compte sur navigateur partagé),
60
+ * ou un jeton porteur non expiré et non révoqué. `true` si aucune re-validation
61
+ * n'a pu être câblée (best-effort, jamais un faux refus).
62
+ *
63
+ * @param nowMs - horloge injectable (epoch ms) ; par défaut l'heure courante.
64
+ * Sert aux bornes temporelles d'un jeton, et rend le comportement testable
65
+ * sans dépendre de l'horloge de la machine.
66
+ */
67
+ isValid(nowMs) {
68
+ return this.#revalidate ? this.#revalidate(nowMs) : Promise.resolve(true);
69
+ }
70
+ getUserIdentifier() {
71
+ return this.#user.identifier;
72
+ }
73
+ isAuthenticated() {
74
+ return true;
75
+ }
76
+ getRoles() {
77
+ return [...this.#user.roles];
78
+ }
79
+ /**
80
+ * Scopes délégués — copie défensive.
81
+ *
82
+ * ⚠️ Ils doivent être RÉELS : `ScopeVoter` ne consulte cette liste que pour un
83
+ * jeton machine. Une liste vide sur un jeton `jwt`/`apikey` ne veut pas dire
84
+ * « tous les droits », elle veut dire « aucun scope » — donc refus.
85
+ */
86
+ getScopes() {
87
+ return [...this.#scopes];
88
+ }
89
+ getAttribute(key) {
90
+ return key === "user" ? this.#user : void 0;
91
+ }
92
+ };
93
+ //#endregion
94
+ export { UserRealtimeToken, UserRealtimeToken as default };