@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.
- package/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { decryptSecret, encryptSecret, generateEphemeralKey } from "../crypto/secretCipher.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dérive la clé AES-256 du domaine TOTP via HKDF-SHA256 (RFC 5869).
|
|
5
|
+
* Déterministe (clé lisible cross-pod). Délègue à {@link deriveKey} avec le
|
|
6
|
+
* contexte figé du domaine TOTP.
|
|
7
|
+
*/
|
|
8
|
+
export declare function deriveTotpKey(material: string | Buffer): Buffer;
|
|
9
|
+
export { encryptSecret, decryptSecret, generateEphemeralKey };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cœur **pur** du 2FA TOTP — RFC 6238 (Time-based OTP) au-dessus de RFC 4226
|
|
3
|
+
* (HOTP). Aucun I/O, aucun container, aucune persistance → testable sans serveur
|
|
4
|
+
* et partagé par l'émetteur (enrôlement) et le vérificateur (login step-up).
|
|
5
|
+
*
|
|
6
|
+
* **Principe** (RFC 6238 §4) : un secret partagé `K` + un compteur dérivé du
|
|
7
|
+
* temps `T = ⌊(epochSec − T0) / step⌋` remplacent le compteur incrémental de
|
|
8
|
+
* HOTP. Les deux parties (serveur, application d'authentification) calculent
|
|
9
|
+
* `HOTP(K, T) = Truncate(HMAC-SHA-1(K, T))` ; l'horloge fait office de canal de
|
|
10
|
+
* synchronisation — aucun échange réseau par code.
|
|
11
|
+
*
|
|
12
|
+
* **Troncature dynamique** (RFC 4226 §5.3) : l'octet de poids faible du condensat
|
|
13
|
+
* HMAC fournit un offset `0..15` ; on lit 4 octets à cet offset, on masque le bit
|
|
14
|
+
* de signe (`0x7f`, pour lever l'ambiguïté signé/non-signé inter-processeur), puis
|
|
15
|
+
* `code = bin31 mod 10^digits`.
|
|
16
|
+
*
|
|
17
|
+
* **Secret** : RFC 4226 R6 exige ≥ 128 bits, RECOMMANDE 160 bits → 20 octets
|
|
18
|
+
* (= taille du bloc HMAC-SHA-1, optimal). Encodé en base32 RFC 4648 (sans padding)
|
|
19
|
+
* pour l'URI `otpauth://` que lisent Google Authenticator / Authy / 1Password.
|
|
20
|
+
*/
|
|
21
|
+
/** Algorithmes HMAC admis (RFC 6238 §1.2 : SHA1 par défaut, SHA256/512 optionnels). */
|
|
22
|
+
export type TotpAlgorithm = "SHA1" | "SHA256" | "SHA512";
|
|
23
|
+
/** Paramètres TOTP — défauts interopérables (Google Authenticator). */
|
|
24
|
+
export declare const TOTP_DEFAULTS: {
|
|
25
|
+
/** Taille du secret en octets (RFC 4226 R6 : 160 bits recommandés). */
|
|
26
|
+
readonly secretBytes: 20;
|
|
27
|
+
/** Période d'un code en secondes (RFC 6238 §5.2 : 30 s recommandé). */
|
|
28
|
+
readonly step: 30;
|
|
29
|
+
/** Nombre de chiffres du code (RFC 4226 §5.3 : 6 minimum). */
|
|
30
|
+
readonly digits: 6;
|
|
31
|
+
/** Fonction de hachage HMAC (compat maximale = SHA1). */
|
|
32
|
+
readonly algorithm: TotpAlgorithm;
|
|
33
|
+
/** Origine du décompte des tranches (RFC 6238 : T0 = 0 = epoch Unix). */
|
|
34
|
+
readonly t0: 0;
|
|
35
|
+
/**
|
|
36
|
+
* Tolérance de dérive d'horloge, en nombre de pas de part et d'autre.
|
|
37
|
+
* RFC 6238 §5.2 : « at most one time step » → ±1 (1 pas = 30 s avant/après).
|
|
38
|
+
*/
|
|
39
|
+
readonly window: 1;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Encode des octets en base32 RFC 4648 **sans padding** (`=`) — forme attendue
|
|
43
|
+
* par les applications d'authentification dans l'URI `otpauth://`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function base32Encode(buf: Buffer): string;
|
|
46
|
+
/**
|
|
47
|
+
* Décode une chaîne base32 RFC 4648 → octets. Tolérant : ignore espaces,
|
|
48
|
+
* tirets et padding `=`, insensible à la casse (un secret peut être saisi à la
|
|
49
|
+
* main par l'utilisateur). Lève si un caractère hors alphabet est rencontré.
|
|
50
|
+
*/
|
|
51
|
+
export declare function base32Decode(input: string): Buffer;
|
|
52
|
+
/** Génère un secret TOTP cryptographiquement aléatoire (défaut 160 bits). */
|
|
53
|
+
export declare function generateTotpSecret(bytes?: number): Buffer;
|
|
54
|
+
/**
|
|
55
|
+
* HOTP (RFC 4226 §5.3) — `Truncate(HMAC(K, counter))` → code à `digits` chiffres,
|
|
56
|
+
* complété à gauche par des zéros.
|
|
57
|
+
*
|
|
58
|
+
* @param secret - secret partagé `K` (octets bruts).
|
|
59
|
+
* @param counter - compteur `C` (≥ 0).
|
|
60
|
+
* @param digits - longueur du code (défaut 6).
|
|
61
|
+
* @param algorithm - fonction HMAC (défaut SHA1).
|
|
62
|
+
*/
|
|
63
|
+
export declare function hotp(secret: Buffer, counter: number, digits?: number, algorithm?: TotpAlgorithm): string;
|
|
64
|
+
/** Numéro de tranche temporelle `T` (RFC 6238 §4.2) pour un instant donné. */
|
|
65
|
+
export declare function totpCounter(epochSec: number, step?: number, t0?: number): number;
|
|
66
|
+
/** Options de calcul/vérification d'un code TOTP. */
|
|
67
|
+
export interface ITotpOptions {
|
|
68
|
+
/** Instant de référence en **millisecondes** (défaut : horloge système). */
|
|
69
|
+
epochMs?: number;
|
|
70
|
+
step?: number;
|
|
71
|
+
digits?: number;
|
|
72
|
+
algorithm?: TotpAlgorithm;
|
|
73
|
+
t0?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calcule le code TOTP courant (RFC 6238 §4) pour un secret donné.
|
|
77
|
+
*
|
|
78
|
+
* @param secret - secret partagé `K`.
|
|
79
|
+
* @returns le code à `digits` chiffres pour la tranche temporelle courante.
|
|
80
|
+
*/
|
|
81
|
+
export declare function totpCode(secret: Buffer, opts?: ITotpOptions): string;
|
|
82
|
+
/** Résultat d'une vérification TOTP — `step` matché = ancre anti-rejeu. */
|
|
83
|
+
export interface ITotpVerifyResult {
|
|
84
|
+
/** Le code présenté correspond à une tranche de la fenêtre de tolérance. */
|
|
85
|
+
valid: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Numéro de tranche `T` qui a validé le code (présent si `valid`). À
|
|
88
|
+
* persister (`lastUsedStep`) pour **refuser tout rejeu** dans la même fenêtre
|
|
89
|
+
* (RFC 6238 §5.2).
|
|
90
|
+
*/
|
|
91
|
+
step?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Vérifie un code TOTP présenté contre une fenêtre de tolérance `±window` pas
|
|
95
|
+
* (RFC 6238 §5.2). Comparaison en **temps constant** (anti-timing). Retourne la
|
|
96
|
+
* tranche `T` validée pour permettre à l'appelant d'appliquer l'anti-rejeu.
|
|
97
|
+
*
|
|
98
|
+
* @param code - code présenté par l'utilisateur (les espaces sont ignorés).
|
|
99
|
+
* @param secret - secret partagé `K`.
|
|
100
|
+
* @param window - nombre de pas de tolérance de part et d'autre (défaut ±1).
|
|
101
|
+
*/
|
|
102
|
+
export declare function verifyTotp(code: string, secret: Buffer, opts?: ITotpOptions & {
|
|
103
|
+
window?: number;
|
|
104
|
+
}): ITotpVerifyResult;
|
|
105
|
+
/** Champs d'un URI `otpauth://totp/...` (Key Uri Format de facto). */
|
|
106
|
+
export interface IOtpauthParams {
|
|
107
|
+
/** Émetteur affiché dans l'application (ex. « Nodefony »). */
|
|
108
|
+
issuer: string;
|
|
109
|
+
/** Compte (ex. identifiant/email de l'utilisateur). */
|
|
110
|
+
account: string;
|
|
111
|
+
/** Secret encodé en base32 RFC 4648. */
|
|
112
|
+
secretBase32: string;
|
|
113
|
+
algorithm?: TotpAlgorithm;
|
|
114
|
+
digits?: number;
|
|
115
|
+
period?: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Construit l'URI `otpauth://totp/{issuer}:{account}?...` encodé dans le QR code
|
|
119
|
+
* d'enrôlement. Le label `issuer:account` et le paramètre `issuer` sont tous deux
|
|
120
|
+
* renseignés (recommandation Key Uri Format pour la compat des lecteurs).
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildOtpauthUri(params: IOtpauthParams): string;
|
|
123
|
+
/**
|
|
124
|
+
* Tire `count` indices UNIFORMES dans `[0, size)` à partir d'octets aléatoires.
|
|
125
|
+
*
|
|
126
|
+
* Le repli direct (`octet % size`) n'est uniforme que si `size` divise 256. Ici
|
|
127
|
+
* `256 % 30 = 16` : les seize premiers symboles de l'alphabet sortaient une fois
|
|
128
|
+
* sur neuf-deux-cent-cinquante-sixièmes, les quatorze autres une fois sur huit —
|
|
129
|
+
* environ 12 % plus souvent pour les premiers.
|
|
130
|
+
*
|
|
131
|
+
* L'enjeu réel est modeste (l'entropie tombe de 4,9069 à 4,9044 bit par
|
|
132
|
+
* caractère, soit 0,025 bit perdu sur les ~49 d'un code) et n'ouvre aucune
|
|
133
|
+
* attaque praticable. Ce n'est pas la raison de corriger : un tirage biaisé dans
|
|
134
|
+
* du code cryptographique est une dette qui ne se voit plus une fois écrite, et
|
|
135
|
+
* dont le coût explose si l'alphabet change un jour pour une taille moins
|
|
136
|
+
* clémente. Le refus d'échantillon coûte ici quelques octets de plus, une fois
|
|
137
|
+
* par activation de second facteur.
|
|
138
|
+
*
|
|
139
|
+
* @param count - nombre d'indices voulus.
|
|
140
|
+
* @param size - taille de l'alphabet (2 à 256).
|
|
141
|
+
* @param source - fournisseur d'octets aléatoires — paramétrable pour que le
|
|
142
|
+
* refus d'échantillon soit OBSERVABLE en test, faute de quoi on ne
|
|
143
|
+
* prouverait jamais que la garde mord.
|
|
144
|
+
* @returns exactement `count` indices, uniformément distribués.
|
|
145
|
+
*/
|
|
146
|
+
export declare function unbiasedIndices(count: number, size: number, source?: (n: number) => Buffer): number[];
|
|
147
|
+
/**
|
|
148
|
+
* Génère `count` codes de récupération à usage unique, lisibles
|
|
149
|
+
* (`XXXXX-XXXXX`, ~50 bits chacun). À présenter **une seule fois** à
|
|
150
|
+
* l'utilisateur ; seul leur condensat est persisté ({@link hashRecoveryCode}).
|
|
151
|
+
*/
|
|
152
|
+
export declare function generateRecoveryCodes(count?: number): string[];
|
|
153
|
+
/**
|
|
154
|
+
* Condensat au repos d'un code de récupération (`sha256` hex). `sha256` suffit
|
|
155
|
+
* (≈ 50 bits aléatoires non brute-forçables, comme une clé API — pas un mot de
|
|
156
|
+
* passe humain) ; la normalisation rend la vérification insensible casse/tirets.
|
|
157
|
+
*/
|
|
158
|
+
export declare function hashRecoveryCode(code: string): string;
|
|
159
|
+
/**
|
|
160
|
+
* Cherche un code de récupération présenté parmi des condensats stockés, en
|
|
161
|
+
* **temps constant par entrée**. Retourne l'index consommé (à retirer du stock),
|
|
162
|
+
* ou `-1` si aucun ne correspond.
|
|
163
|
+
*/
|
|
164
|
+
export declare function matchRecoveryCode(presented: string, hashes: readonly string[]): number;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import type { ITotpSecretStore } from "../../contracts/ITotpSecretStore.js";
|
|
3
|
+
import { type TotpAlgorithm } from "./totpCrypto.js";
|
|
4
|
+
/**
|
|
5
|
+
* Dépendances résolues d'une opération TOTP — **injectées** (store, clé de
|
|
6
|
+
* chiffrement, horloge) pour rendre toute la logique métier testable sans kernel
|
|
7
|
+
* ni serveur. `TotpService` les résout au boot puis délègue ici (même esprit que
|
|
8
|
+
* le verdict neutre d'idempotence).
|
|
9
|
+
*/
|
|
10
|
+
export interface ITotpDeps {
|
|
11
|
+
store: ITotpSecretStore;
|
|
12
|
+
/** Clé AES-256 de (dé)chiffrement du secret au repos (dérivée par le service). */
|
|
13
|
+
key: Buffer;
|
|
14
|
+
/** Horloge en ms — injectable pour les tests ; runtime = `Date.now`. */
|
|
15
|
+
now: () => number;
|
|
16
|
+
issuer: string;
|
|
17
|
+
algorithm: TotpAlgorithm;
|
|
18
|
+
digits: number;
|
|
19
|
+
period: number;
|
|
20
|
+
window: number;
|
|
21
|
+
recoveryCodesCount: number;
|
|
22
|
+
}
|
|
23
|
+
/** Données d'enrôlement présentées **une seule fois** (QR + saisie manuelle). */
|
|
24
|
+
export interface ITotpEnrollment {
|
|
25
|
+
/** Secret en base32 (scanné via le QR ou saisi à la main). */
|
|
26
|
+
secretBase32: string;
|
|
27
|
+
/** URI `otpauth://` encodé dans le QR code. */
|
|
28
|
+
otpauthUri: string;
|
|
29
|
+
}
|
|
30
|
+
/** Résultat de l'activation — codes de récupération **clairs**, affichés 1×. */
|
|
31
|
+
export interface ITotpActivation {
|
|
32
|
+
recoveryCodes: string[];
|
|
33
|
+
}
|
|
34
|
+
/** État 2FA d'un utilisateur (UI self-service / Studio). */
|
|
35
|
+
export interface ITotpStatus {
|
|
36
|
+
/** 2FA activé (enrôlement confirmé). */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** Enrôlement commencé, pas encore confirmé. */
|
|
39
|
+
pending: boolean;
|
|
40
|
+
/** Codes de récupération restants (non consommés). */
|
|
41
|
+
recoveryCodesRemaining: number;
|
|
42
|
+
}
|
|
43
|
+
/** Résultat d'une vérification de second facteur au login. */
|
|
44
|
+
export interface ITotpLoginResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
/** Méthode ayant validé (utile pour l'audit). */
|
|
47
|
+
method?: "totp" | "recovery";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Démarre l'enrôlement : génère un secret aléatoire, le **chiffre** au repos et
|
|
51
|
+
* l'enregistre en attente de confirmation (`confirmedAt: null`). Retourne le
|
|
52
|
+
* secret en clair (base32 + URI) — **seul moment** où il est exposé. Idempotent :
|
|
53
|
+
* un nouvel appel écrase un enrôlement non confirmé (re-scan du QR).
|
|
54
|
+
*/
|
|
55
|
+
export declare function beginTotpEnrollment(deps: ITotpDeps, userId: string, account: string): Promise<ITotpEnrollment>;
|
|
56
|
+
/**
|
|
57
|
+
* Confirme l'enrôlement : vérifie un 1ᵉʳ code généré par l'app, **active** le 2FA
|
|
58
|
+
* et génère les codes de récupération (retournés clairs 1×, hachés au repos). Le
|
|
59
|
+
* step de confirmation est marqué consommé (anti-rejeu). Lève si aucun enrôlement
|
|
60
|
+
* n'est en cours, s'il est déjà confirmé, ou si le code est invalide (reste pending).
|
|
61
|
+
*/
|
|
62
|
+
export declare function confirmTotpEnrollment(deps: ITotpDeps, userId: string, code: string): Promise<ITotpActivation>;
|
|
63
|
+
/**
|
|
64
|
+
* Vérifie un second facteur au login : d'abord un code TOTP (fenêtre ±window,
|
|
65
|
+
* **anti-rejeu** via `lastUsedStep`), à défaut un code de récupération (consommé,
|
|
66
|
+
* usage unique). Retourne `ok: false` si le 2FA n'est pas activé ou si rien ne
|
|
67
|
+
* correspond — **jamais d'exception** (chemin d'authentification).
|
|
68
|
+
*/
|
|
69
|
+
export declare function verifyTotpLogin(deps: ITotpDeps, userId: string, code: string): Promise<ITotpLoginResult>;
|
|
70
|
+
/** Désactive le 2FA (retire le secret et les codes de récupération). */
|
|
71
|
+
export declare function disableTotp(deps: ITotpDeps, userId: string): Promise<void>;
|
|
72
|
+
/** État 2FA d'un utilisateur (absent / pending / activé + codes restants). */
|
|
73
|
+
export declare function totpStatus(deps: ITotpDeps, userId: string): Promise<ITotpStatus>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
3
|
+
import type { ITotpSecretStore } from "../../contracts/ITotpSecretStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registre de **fabriques de stores de secrets TOTP** — résout un nom (`memory`,
|
|
6
|
+
* `file`, `drizzle`, `mongoose`, `redis`…) vers une instance, SANS coupler le cœur
|
|
7
|
+
* à un backend en dur.
|
|
8
|
+
*
|
|
9
|
+
* Convention-frère de `webAuthnCredentialStoreRegistry` / `tokenStoreRegistry` :
|
|
10
|
+
* les builtins sans dépendance (`memory`, `file`) s'enregistrent au chargement du
|
|
11
|
+
* module ; les adapters lourds s'enregistrent depuis LEUR module (ils importent
|
|
12
|
+
* `import type { ITotpSecretStore }`, effacé à la compilation → 0 dép runtime).
|
|
13
|
+
*/
|
|
14
|
+
export interface ITotpStoreFactoryContext {
|
|
15
|
+
/** Container DI — résolution de services (ORM, redis…). */
|
|
16
|
+
readonly container: Container;
|
|
17
|
+
/** Config sécurité validée + gelée. */
|
|
18
|
+
readonly config: ISecurityConfig;
|
|
19
|
+
}
|
|
20
|
+
/** Fabrique d'un store de secrets TOTP pour un nom donné. */
|
|
21
|
+
export type TotpStoreFactory = (ctx: ITotpStoreFactoryContext) => ITotpSecretStore;
|
|
22
|
+
/** Enregistre (ou remplace) la fabrique d'un store de secrets TOTP. */
|
|
23
|
+
export declare function registerTotpStore(name: string, factory: TotpStoreFactory): void;
|
|
24
|
+
/** Fabrique d'un store par nom, ou `undefined` si inconnu. */
|
|
25
|
+
export declare function getTotpStoreFactory(name: string): TotpStoreFactory | undefined;
|
|
26
|
+
/** Noms enregistrés (validation boot, introspection Studio, tests). */
|
|
27
|
+
export declare function listTotpStores(): string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { IAccessVoter } from "../../contracts/IAccessVoter.js";
|
|
3
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
4
|
+
import { VoterVote } from "../../contracts/IAccessVoter.js";
|
|
5
|
+
/**
|
|
6
|
+
* Voter built-in **niveau A** — résout les attributs `ROLE_*` via la hiérarchie
|
|
7
|
+
* de rôles ({@link RoleHierarchyWalker}).
|
|
8
|
+
*
|
|
9
|
+
* Vote `GRANT` si l'utilisateur possède le rôle (hiérarchie résolue : `ROLE_ADMIN`
|
|
10
|
+
* hérite `ROLE_USER`), **`ABSTAIN` sinon** — jamais `DENY` : l'absence d'un rôle
|
|
11
|
+
* ne doit pas opposer son veto aux autres axes (scope, ownership). C'est le
|
|
12
|
+
* `default DENY` de l'`AuthorizationService` (tous ABSTAIN → refus) qui ferme la
|
|
13
|
+
* porte, pas ce voter.
|
|
14
|
+
*
|
|
15
|
+
* Lit la hiérarchie depuis le container (`roleHierarchy`, posée par le firewall
|
|
16
|
+
* au boot) en lazy — un walker vide gère quand même les rôles plats.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RoleVoter implements IAccessVoter {
|
|
19
|
+
#private;
|
|
20
|
+
private readonly container;
|
|
21
|
+
constructor(container: Container);
|
|
22
|
+
supports(attribute: string): boolean;
|
|
23
|
+
vote(token: IToken, attribute: string): Promise<VoterVote>;
|
|
24
|
+
}
|
|
25
|
+
export default RoleVoter;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IAccessVoter } from "../../contracts/IAccessVoter.js";
|
|
2
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
3
|
+
import { VoterVote } from "../../contracts/IAccessVoter.js";
|
|
4
|
+
/**
|
|
5
|
+
* Voter built-in **axe SCOPE** (P6.8) — applique les scopes `api:action` déclarés
|
|
6
|
+
* par `@RequireScope`. Frère du {@link RoleVoter} sur l'autre axe : les rôles
|
|
7
|
+
* disent QUI tu es, les scopes disent ce qu'une CLÉ déléguée a le droit de faire.
|
|
8
|
+
*
|
|
9
|
+
* `supports()` ne capte QUE la forme conventionnée `api:action` (un `:`, jamais
|
|
10
|
+
* `ROLE_*`) → aucune collision avec le `RoleVoter` ni un voter métier (`doc.edit`).
|
|
11
|
+
*
|
|
12
|
+
* Vote :
|
|
13
|
+
* - **jeton non scopable** (humain/anonyme) → `GRANT` : le scope est un no-op,
|
|
14
|
+
* l'autorisation de l'humain est portée par ses rôles (`@IsGranted`), pas par
|
|
15
|
+
* un downscoping de clé ;
|
|
16
|
+
* - **jeton scopable** (clé API / JWT / OAuth) → `GRANT` si le scope exact est
|
|
17
|
+
* présent, sinon **`ABSTAIN`** (jamais `DENY` : l'absence d'un scope ne doit pas
|
|
18
|
+
* opposer un veto aux autres attributs OR d'une clause — c'est le default-DENY
|
|
19
|
+
* de l'`AuthorizationService`, tous ABSTAIN → refus, qui ferme la porte ;
|
|
20
|
+
* posture identique au `RoleVoter`).
|
|
21
|
+
*
|
|
22
|
+
* Pur : aucune dépendance (ni container, ni I/O) — il ne lit que le jeton déjà
|
|
23
|
+
* résolu au handshake/à l'authentification. Instancié UNE fois au boot.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ScopeVoter implements IAccessVoter {
|
|
26
|
+
/** Capte les attributs scope `api:action` — ni `ROLE_*`, ni attribut métier sans `:`. */
|
|
27
|
+
supports(attribute: string): boolean;
|
|
28
|
+
vote(token: IToken, attribute: string): Promise<VoterVote>;
|
|
29
|
+
}
|
|
30
|
+
export default ScopeVoter;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { IAccessVoter } from "../../contracts/IAccessVoter.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registre de **fabriques de voters** — alimente l'`AuthorizationService` au boot
|
|
5
|
+
* SANS qu'il connaisse le moindre voter en dur.
|
|
6
|
+
*
|
|
7
|
+
* Pourquoi un registre (et pas un scan DI « tous les `@injectable` implémentant
|
|
8
|
+
* `IAccessVoter` ») : les interfaces TypeScript sont **effacées à la compilation**
|
|
9
|
+
* — rien à scanner au runtime. Le registre EST le marqueur explicite. Un voter
|
|
10
|
+
* built-in s'enregistre au chargement du module (toujours avant le boot) ; un
|
|
11
|
+
* voter métier (app/plugin) appelle `registerVoterFactory("projectVoter", …)`
|
|
12
|
+
* puis il est découvert automatiquement — aucun changement dans le cœur.
|
|
13
|
+
* Convention-frère : `authenticatorRegistry`, `tokenStoreRegistry`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Contexte passé à une fabrique de voter : le container DI pour résoudre les
|
|
17
|
+
* services dont le voter a besoin (repository, hiérarchie de rôles…). La fabrique
|
|
18
|
+
* ne fait QUE construire — les résolutions coûteuses restent lazy dans l'instance.
|
|
19
|
+
*/
|
|
20
|
+
export interface IVoterFactoryContext {
|
|
21
|
+
/** Container DI — résolution de services (`roleHierarchy`, repositories…). */
|
|
22
|
+
readonly container: Container;
|
|
23
|
+
}
|
|
24
|
+
/** Fabrique d'un voter pour un nom donné. */
|
|
25
|
+
export type VoterFactory = (ctx: IVoterFactoryContext) => IAccessVoter;
|
|
26
|
+
/**
|
|
27
|
+
* Enregistre (ou remplace) la fabrique d'un voter. Appelé par les builtins au
|
|
28
|
+
* chargement du module, et par les apps/plugins pour les leurs (`ProjectVoter`,
|
|
29
|
+
* `TenantVoter`…).
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerVoterFactory(name: string, factory: VoterFactory): void;
|
|
32
|
+
/** Toutes les fabriques enregistrées (consommées par l'`AuthorizationService`). */
|
|
33
|
+
export declare function listVoterFactories(): ReadonlyMap<string, VoterFactory>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type { IWebAuthnCredential } from "../../contracts/IWebAuthnCredential.js";
|
|
3
|
+
import type { IWebAuthnCredentialStore, IWebAuthnCredentialSummary, IWebAuthnListQuery, WebAuthnAuthUpdate } from "../../contracts/IWebAuthnCredentialStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Projection contractuelle : credential complet → vue admin **sans `publicKey`**.
|
|
6
|
+
* Partagée par les backends qui matérialisent des credentials en mémoire.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toWebAuthnSummary(c: IWebAuthnCredential): IWebAuthnCredentialSummary;
|
|
9
|
+
/**
|
|
10
|
+
* Store de credentials WebAuthn **en mémoire** — implémentation de référence
|
|
11
|
+
* d'{@link IWebAuthnCredentialStore}.
|
|
12
|
+
*
|
|
13
|
+
* 0 dépendance, idéale pour le développement mono-process et les **tests**. NON
|
|
14
|
+
* partagée entre process (pas de cluster) et **volatile** (perdue au
|
|
15
|
+
* redémarrage) → en production multi-process, utiliser un adapter ORM ou Redis.
|
|
16
|
+
*
|
|
17
|
+
* Perf/mémoire : les `Map` n'existent que si le store est instancié (passkeys
|
|
18
|
+
* activés), jamais sur le hot path par requête HTTP.
|
|
19
|
+
*/
|
|
20
|
+
/** Instantané sérialisable de l'état — base de la persistance fichier. */
|
|
21
|
+
export interface WebAuthnStoreSnapshot {
|
|
22
|
+
credentials: IWebAuthnCredential[];
|
|
23
|
+
}
|
|
24
|
+
export declare class MemoryWebAuthnCredentialStore implements IWebAuthnCredentialStore {
|
|
25
|
+
#private;
|
|
26
|
+
findById(credentialId: string): Promise<IWebAuthnCredential | null>;
|
|
27
|
+
findByUser(userId: string): Promise<IWebAuthnCredential[]>;
|
|
28
|
+
countByUser(userId: string): Promise<number>;
|
|
29
|
+
save(credential: IWebAuthnCredential): Promise<void>;
|
|
30
|
+
update(credentialId: string, patch: WebAuthnAuthUpdate): Promise<void>;
|
|
31
|
+
delete(credentialId: string): Promise<void>;
|
|
32
|
+
listPage(query: IWebAuthnListQuery): Promise<IPage<IWebAuthnCredentialSummary>>;
|
|
33
|
+
countCredentials(query: IWebAuthnListQuery): Promise<number>;
|
|
34
|
+
/** Instantané sérialisable de l'état courant (pour la persistance fichier). */
|
|
35
|
+
snapshot(): WebAuthnStoreSnapshot;
|
|
36
|
+
/** Remplace l'état par celui d'un instantané (reconstruit l'index par user). */
|
|
37
|
+
restore(snapshot: WebAuthnStoreSnapshot): void;
|
|
38
|
+
}
|
|
39
|
+
export default MemoryWebAuthnCredentialStore;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
3
|
+
import type { IWebAuthnCredentialStore } from "../../contracts/IWebAuthnCredentialStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registre de **fabriques de stores de credentials WebAuthn** — résout un nom
|
|
6
|
+
* (`memory`, `drizzle`, `mongoose`, `redis`…) vers une instance, SANS coupler le
|
|
7
|
+
* cœur à un backend en dur.
|
|
8
|
+
*
|
|
9
|
+
* Convention-frère de `tokenStoreRegistry` : le builtin `memory` s'enregistre au
|
|
10
|
+
* chargement du module ; les adapters lourds s'enregistrent depuis LEUR module
|
|
11
|
+
* (ils importent `import type { IWebAuthnCredentialStore }`, effacé à la compilation).
|
|
12
|
+
*/
|
|
13
|
+
export interface IWebAuthnStoreFactoryContext {
|
|
14
|
+
/** Container DI — résolution de services (ORM, redis…). */
|
|
15
|
+
readonly container: Container;
|
|
16
|
+
/** Config sécurité validée + gelée. */
|
|
17
|
+
readonly config: ISecurityConfig;
|
|
18
|
+
}
|
|
19
|
+
/** Fabrique d'un store de credentials pour un nom donné. */
|
|
20
|
+
export type WebAuthnStoreFactory = (ctx: IWebAuthnStoreFactoryContext) => IWebAuthnCredentialStore;
|
|
21
|
+
/** Enregistre (ou remplace) la fabrique d'un store de credentials WebAuthn. */
|
|
22
|
+
export declare function registerWebAuthnStore(name: string, factory: WebAuthnStoreFactory): void;
|
|
23
|
+
/** Fabrique d'un store par nom, ou `undefined` si inconnu. */
|
|
24
|
+
export declare function getWebAuthnStoreFactory(name: string): WebAuthnStoreFactory | undefined;
|
|
25
|
+
/** Noms enregistrés (validation boot, introspection Studio, tests). */
|
|
26
|
+
export declare function listWebAuthnStores(): string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type { IWebhookListQuery, IWebhookStore } from "../../contracts/IWebhookStore.js";
|
|
3
|
+
import type { IWebhookEndpoint, WebhookEndpointUpdate } from "../../contracts/IWebhookEndpoint.js";
|
|
4
|
+
/**
|
|
5
|
+
* Applique les filtres d'{@link IWebhookListQuery} à un endpoint — sémantique de
|
|
6
|
+
* RÉFÉRENCE du contrat, partagée par les backends qui filtrent en mémoire.
|
|
7
|
+
*
|
|
8
|
+
* @param e - endpoint candidat.
|
|
9
|
+
* @param query - filtres du listing (les champs omis ne filtrent pas).
|
|
10
|
+
* @returns `true` si l'endpoint appartient à la collection filtrée.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchesWebhookQuery(e: IWebhookEndpoint, query: IWebhookListQuery): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Store d'endpoints webhook **en mémoire** — défaut dev/test (non persistant :
|
|
15
|
+
* les endpoints sont perdus au redémarrage). En prod, utiliser `drizzle`/
|
|
16
|
+
* `mongoose` (config `webhooks.store`).
|
|
17
|
+
*
|
|
18
|
+
* Map indexée par id (O(1)). Les lectures renvoient une **copie défensive** : le
|
|
19
|
+
* store détient la vérité, un consommateur ne peut pas muter un record en place.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MemoryWebhookStore implements IWebhookStore {
|
|
22
|
+
#private;
|
|
23
|
+
/**
|
|
24
|
+
* {@inheritDoc IWebhookStore.sortableFields}
|
|
25
|
+
*
|
|
26
|
+
* Le store porte l'endpoint complet : il sait trier tout le vocabulaire
|
|
27
|
+
* public, sans réduction de capacité.
|
|
28
|
+
*/
|
|
29
|
+
readonly sortableFields: readonly ["createdAt", "updatedAt", "url", "enabled", "failureCount", "id"];
|
|
30
|
+
save(endpoint: IWebhookEndpoint): Promise<void>;
|
|
31
|
+
findById(id: string): Promise<IWebhookEndpoint | null>;
|
|
32
|
+
update(id: string, patch: WebhookEndpointUpdate): Promise<void>;
|
|
33
|
+
delete(id: string): Promise<void>;
|
|
34
|
+
listAll(): Promise<IWebhookEndpoint[]>;
|
|
35
|
+
listPage(query: IWebhookListQuery): Promise<IPage<IWebhookEndpoint>>;
|
|
36
|
+
countEndpoints(query: IWebhookListQuery): Promise<number>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { IAuditEvent } from "../../contracts/IAuditEvent.js";
|
|
2
|
+
import type { IWebhookEndpoint } from "../../contracts/IWebhookEndpoint.js";
|
|
3
|
+
import type { IWebhookDeliveryPolicy } from "../../service/webhooks.js";
|
|
4
|
+
import type { IDeliveryResult } from "./webhookDelivery.js";
|
|
5
|
+
/** Une souscription matche-t-elle une action d'audit ? `*` = toutes, `x.*` = préfixe. */
|
|
6
|
+
export declare function matchesSubscription(patterns: readonly string[], action: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Classe le résultat d'une livraison : 2xx = succès ; réseau/timeout/429/408/5xx =
|
|
9
|
+
* réessayable ; 3xx/4xx (config cliente erronée) = échec définitif (pas de retry).
|
|
10
|
+
*/
|
|
11
|
+
export declare function classifyDelivery(r: IDeliveryResult): "success" | "retry" | "fail";
|
|
12
|
+
/** Backoff exponentiel déterministe (jitter cross-pod = slice Redis cluster). */
|
|
13
|
+
export declare function backoffMs(attempt: number): number;
|
|
14
|
+
/** Trace d'une livraison terminée, poussée à `recordDelivery` (historique). */
|
|
15
|
+
export interface IWebhookDeliveryRecord {
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
readonly type: string;
|
|
18
|
+
readonly attempt: number;
|
|
19
|
+
readonly ok: boolean;
|
|
20
|
+
readonly status: number | null;
|
|
21
|
+
readonly error: string | null;
|
|
22
|
+
readonly durationMs: number;
|
|
23
|
+
readonly requestBody: string;
|
|
24
|
+
readonly responseBody: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** Dépendances injectées du dispatcher (E/S + temps + politique). */
|
|
27
|
+
export interface IWebhookDispatcherDeps {
|
|
28
|
+
/** Nombre d'endpoints (0-alloc) — court-circuit hot-path. */
|
|
29
|
+
endpointCount(): number;
|
|
30
|
+
/** Snapshot des endpoints (n'est lu que si `endpointCount() > 0`). */
|
|
31
|
+
getSnapshot(): IWebhookEndpoint[];
|
|
32
|
+
/** Secret de signature en clair (`whsec_…`) d'un endpoint. */
|
|
33
|
+
secretOf(endpoint: IWebhookEndpoint): string;
|
|
34
|
+
/** Politique de livraison (tolérance/retries/timeout/concurrence/file). */
|
|
35
|
+
readonly policy: IWebhookDeliveryPolicy;
|
|
36
|
+
/** Re-contrôle SSRF + renvoie les IP à pinner ; **lève** si la cible est interdite. */
|
|
37
|
+
resolveTarget(url: string): Promise<string[]>;
|
|
38
|
+
/** Émet la requête HTTP signée (injecté → testable). */
|
|
39
|
+
deliver(url: string, body: string, headers: Record<string, string>, opts: {
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
addresses: string[];
|
|
42
|
+
allowHttp: boolean;
|
|
43
|
+
}): Promise<IDeliveryResult>;
|
|
44
|
+
/** Met à jour l'endpoint (lastDelivery, failureCount, auto-disable). */
|
|
45
|
+
markDelivery(id: string, result: IDeliveryResult): void | Promise<void>;
|
|
46
|
+
/** Enregistre une trace de livraison (historique « récentes », par endpoint). */
|
|
47
|
+
recordDelivery(id: string, rec: IWebhookDeliveryRecord): void;
|
|
48
|
+
/** Horloge injectable. */
|
|
49
|
+
now(): number;
|
|
50
|
+
/** Génère un `webhook-id` de message. */
|
|
51
|
+
newMessageId(): string;
|
|
52
|
+
/** Planifie un retry ; retourne une fonction d'annulation. */
|
|
53
|
+
schedule(fn: () => void, ms: number): () => void;
|
|
54
|
+
/** Journalisation (saturation / erreurs inattendues). */
|
|
55
|
+
log(message: string): void;
|
|
56
|
+
}
|
|
57
|
+
export declare class WebhookDispatcher {
|
|
58
|
+
#private;
|
|
59
|
+
constructor(deps: IWebhookDispatcherDeps);
|
|
60
|
+
/**
|
|
61
|
+
* Réagit à un événement d'audit. **Hot-path** : court-circuit à coût nul si
|
|
62
|
+
* aucun endpoint ; sinon filtre et empile (le travail lourd est différé).
|
|
63
|
+
*/
|
|
64
|
+
onAuditEvent(event: IAuditEvent): void;
|
|
65
|
+
/** Livraisons abandonnées pour cause de file pleine (observabilité). */
|
|
66
|
+
droppedCount(): number;
|
|
67
|
+
/** Arrêt propre : stoppe l'admission, annule les retries, vide la file. */
|
|
68
|
+
shutdown(): void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { decryptSecret, encryptSecret, generateEphemeralKey } from "../crypto/secretCipher.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dérive la clé AES-256 du domaine webhook via HKDF-SHA256 (RFC 5869).
|
|
5
|
+
* Déterministe (clé lisible cross-pod).
|
|
6
|
+
*/
|
|
7
|
+
export declare function deriveWebhookKey(material: string | Buffer): Buffer;
|
|
8
|
+
export { encryptSecret, decryptSecret, generateEphemeralKey };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Résultat d'une tentative de livraison. */
|
|
2
|
+
export interface IDeliveryResult {
|
|
3
|
+
/** Livraison acceptée (2xx) ? */
|
|
4
|
+
readonly ok: boolean;
|
|
5
|
+
/** Code HTTP, ou `null` si l'échec est réseau/timeout (pas de réponse). */
|
|
6
|
+
readonly status: number | null;
|
|
7
|
+
/** Message d'erreur (réseau/timeout/HTTP non-2xx), ou `null` si OK. */
|
|
8
|
+
readonly error: string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Début du corps de réponse du destinataire (tronqué à {@link RESPONSE_BODY_CAP}),
|
|
11
|
+
* ou `null` si vide/réseau. Capturé pour l'historique de livraison (debug).
|
|
12
|
+
*/
|
|
13
|
+
readonly responseBody?: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** Options de transport d'une livraison. */
|
|
16
|
+
export interface IDeliveryOptions {
|
|
17
|
+
/** Délai max de la tentative (ms). */
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
/** IP validées à **pinner** (la 1ʳᵉ est utilisée) ; vide = résolution normale. */
|
|
20
|
+
readonly addresses?: readonly string[];
|
|
21
|
+
/** Autorise `http:` (dev). Défaut : https only. */
|
|
22
|
+
readonly allowHttp?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* POST le corps signé vers l'URL, en pinnant l'IP validée et sans suivre les
|
|
26
|
+
* redirections. Ne lève jamais : tout échec est encodé dans {@link IDeliveryResult}.
|
|
27
|
+
*/
|
|
28
|
+
export declare function deliverWebhook(url: string, body: string, headers: Record<string, string>, opts: IDeliveryOptions): Promise<IDeliveryResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FacetCounts } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de filtre des endpoints webhook**, en noms PUBLICS — ceux
|
|
4
|
+
* qu'un client écrit dans l'URL (`?enabled=false&event=user.created`).
|
|
5
|
+
*
|
|
6
|
+
* Frère de `WEBHOOK_SORTABLE_FIELDS`, et posé au même endroit pour la même
|
|
7
|
+
* raison : le vocabulaire appartient au propriétaire du contrat, la mécanique de
|
|
8
|
+
* lecture au cœur (`parseFilters`).
|
|
9
|
+
*
|
|
10
|
+
* `event` reste une chaîne LIBRE, et non une énumération : le catalogue
|
|
11
|
+
* d'événements est celui de l'application qui émet, pas du framework. Le refus
|
|
12
|
+
* porte donc sur la forme (paramètre inconnu, valeur mal formée), jamais sur le
|
|
13
|
+
* nom d'un événement — le refuser reviendrait à décider à la place de l'app ce
|
|
14
|
+
* qu'elle a le droit de publier.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WEBHOOK_FILTERS: {
|
|
17
|
+
/** `true` = actifs seulement, `false` = désactivés seulement, absent = les deux. */
|
|
18
|
+
readonly enabled: "boolean";
|
|
19
|
+
/** Endpoints abonnés à CET événement (« qui écoute `user.created` ? »). */
|
|
20
|
+
readonly event: "string";
|
|
21
|
+
/** `true` = en échec (`failureCount > 0`), `false` = sains, absent = les deux. */
|
|
22
|
+
readonly failing: "boolean";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* **Les facettes des endpoints** — les questions fermées que la console pose à
|
|
26
|
+
* la collection ENTIÈRE pour ses cartes de tête.
|
|
27
|
+
*
|
|
28
|
+
* Chacune est un filtre du contrat, et c'est la règle : une carte affiche un
|
|
29
|
+
* nombre que le tableau doit pouvoir montrer. `failing` recoupe volontairement
|
|
30
|
+
* `active` et `disabled` — un endpoint peut être actif ET en échec — d'où
|
|
31
|
+
* l'interdiction de déduire une facette d'une autre par soustraction.
|
|
32
|
+
*/
|
|
33
|
+
export declare const WEBHOOK_FACETS: {
|
|
34
|
+
/** Tous les endpoints configurés. */
|
|
35
|
+
readonly total: {};
|
|
36
|
+
/** Endpoints actifs (le dispatcher leur livre). */
|
|
37
|
+
readonly active: {
|
|
38
|
+
readonly enabled: true;
|
|
39
|
+
};
|
|
40
|
+
/** Endpoints désactivés — à la main ou par coupe-circuit. */
|
|
41
|
+
readonly disabled: {
|
|
42
|
+
readonly enabled: false;
|
|
43
|
+
};
|
|
44
|
+
/** Endpoints en échec, actifs ou non. */
|
|
45
|
+
readonly failing: {
|
|
46
|
+
readonly failing: true;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Les compteurs rendus par `GET /nodefony/security/api/webhooks/stats`. */
|
|
50
|
+
export type IWebhookCounts = FacetCounts<typeof WEBHOOK_FACETS>;
|
|
51
|
+
/**
|
|
52
|
+
* Ce que l'endpoint de COMPTEURS accepte de filtrer — `WEBHOOK_FILTERS` **moins**
|
|
53
|
+
* les champs que les facettes décomposent (`enabled`, `failing`).
|
|
54
|
+
*
|
|
55
|
+
* Les demander ici rendrait une réponse contradictoire : le total suivrait le
|
|
56
|
+
* filtre pendant que chaque facette l'écraserait par le sien. `event` reste,
|
|
57
|
+
* parce qu'il découpe une AUTRE dimension — « combien d'endpoints écoutent
|
|
58
|
+
* `user.created`, et dans quel état sont-ils ? » est une question cohérente.
|
|
59
|
+
*
|
|
60
|
+
* Un test verrouille l'accord entre cette liste et {@link WEBHOOK_FACETS}.
|
|
61
|
+
*/
|
|
62
|
+
export declare const WEBHOOK_STATS_FILTERS: {
|
|
63
|
+
readonly event: "string";
|
|
64
|
+
};
|