@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,66 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
/** Données à porter en session entre `authorize` et `callback` (anti-replay). */
|
|
3
|
+
export interface IOAuthAuthorization {
|
|
4
|
+
/** URL d'autorisation vers laquelle rediriger l'utilisateur. */
|
|
5
|
+
readonly url: string;
|
|
6
|
+
/** `state` anti-CSRF à stocker en session (RFC 9700). */
|
|
7
|
+
readonly state: string;
|
|
8
|
+
/** `code_verifier` PKCE à stocker en session, ou `null` (fournisseur sans PKCE). */
|
|
9
|
+
readonly codeVerifier: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*
|
|
13
|
+
* au-dessus d'`arctic`.
|
|
14
|
+
*
|
|
15
|
+
* Posture OAuth 2.1 (RFC 9700) : Authorization Code uniquement (jamais implicit /
|
|
16
|
+
* ROPC), **PKCE S256** quand le fournisseur le supporte (RFC 7636), **state**
|
|
17
|
+
* anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton n'atteint le navigateur
|
|
18
|
+
* (le login produit une **session BFF**, gérée hors de ce service par le
|
|
19
|
+
* controller + `AuthFlow`).
|
|
20
|
+
*
|
|
21
|
+
* `arctic` est **importé paresseusement** au premier login (cold path — jamais au
|
|
22
|
+
* boot ni par requête), comme `@simplewebauthn`/`jose`. Au boot (si
|
|
23
|
+
* `oauth2.enabled`) : seule la config est validée et les fournisseurs configurés
|
|
24
|
+
* sont confrontés au registre (un nom inconnu = WARNING, pas fatal).
|
|
25
|
+
*
|
|
26
|
+
* Le service ne touche **ni HTTP ni session** : il rend à l'appelant les éléments
|
|
27
|
+
* (URL, state, verifier) que le controller persiste en session — testable sans
|
|
28
|
+
* transport, comme `AuthFlow`.
|
|
29
|
+
*/
|
|
30
|
+
declare class OAuth2Service extends Service {
|
|
31
|
+
#private;
|
|
32
|
+
module: Module;
|
|
33
|
+
constructor(module: Module);
|
|
34
|
+
/** `true` si le social login est opérationnel (activé + boot OK). */
|
|
35
|
+
isEnabled(): boolean;
|
|
36
|
+
/** Noms des fournisseurs configurés ET connus du registre (UI : boutons à afficher). */
|
|
37
|
+
listProviders(): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Redirections post-login (succès / échec) — lues par le controller.
|
|
40
|
+
* Surcharge PAR FOURNISSEUR si fournie, sinon valeur globale, sinon défaut.
|
|
41
|
+
*/
|
|
42
|
+
getRedirects(provider?: string): {
|
|
43
|
+
success: string;
|
|
44
|
+
failure: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Étape 1 — prépare l'URL d'autorisation + les éléments anti-replay à stocker
|
|
48
|
+
* en session (`state`, et `code_verifier` si PKCE).
|
|
49
|
+
*
|
|
50
|
+
* @throws AuthenticationError — fournisseur non configuré / inconnu du registre.
|
|
51
|
+
*/
|
|
52
|
+
createAuthorization(provider: string): Promise<IOAuthAuthorization>;
|
|
53
|
+
/**
|
|
54
|
+
* Étape 2 — valide la réponse, échange le `code`, lit le profil et provisionne
|
|
55
|
+
* l'utilisateur local (Shadow User). Retourne l'identifiant à ouvrir en session.
|
|
56
|
+
*
|
|
57
|
+
* @param returnedIss - paramètre `iss` reçu (anti-mix-up RFC 9207), ou `null`.
|
|
58
|
+
* @throws AuthenticationError — `iss` invalide, échange refusé, ou provisioning
|
|
59
|
+
* impossible (lien inconnu + signup interdit).
|
|
60
|
+
*/
|
|
61
|
+
exchangeAndProvision(provider: string, code: string, codeVerifier: string | null, returnedIss: string | null): Promise<{
|
|
62
|
+
identifier: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
export default OAuth2Service;
|
|
66
|
+
export { OAuth2Service };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type CspFragment } from "../src/csp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sous-ensemble APPLICATIF de la config `headers` (cf defineSecurityConfig).
|
|
4
|
+
*
|
|
5
|
+
* `hsts` / `frameguard` / `noSniff` n'y figurent PAS volontairement : ces trois
|
|
6
|
+
* en-têtes « transport » sont posés par `@nodefony/http` à l'entrée brute
|
|
7
|
+
* (`onHttpRequest`, AVANT le pipeline) pour couvrir AUSSI les fichiers statiques,
|
|
8
|
+
* les erreurs précoces et un serveur sans module security (secure-by-default).
|
|
9
|
+
* `@nodefony/security` ne les ré-émet pas → une seule source par en-tête.
|
|
10
|
+
*/
|
|
11
|
+
export interface ISecurityHeadersOptions {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
csp: string;
|
|
14
|
+
cspNonces: boolean;
|
|
15
|
+
referrerPolicy: string;
|
|
16
|
+
coop?: string;
|
|
17
|
+
coep?: string;
|
|
18
|
+
corp?: string;
|
|
19
|
+
originAgentCluster?: boolean;
|
|
20
|
+
permissionsPolicy?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* En-têtes de sécurité **applicatifs** de Nodefony (couche `@nodefony/security`,
|
|
24
|
+
* complémentaire du socle transport de `@nodefony/http`). Pré-calcule une fois au
|
|
25
|
+
* boot la table d'en-têtes CONSTANTS (CSP statique, Referrer-Policy, isolation
|
|
26
|
+
* cross-origin COOP/COEP/CORP, Origin-Agent-Cluster, Permissions-Policy) → zéro
|
|
27
|
+
* alloc, zéro concat par requête (le firewall la pose telle quelle).
|
|
28
|
+
*
|
|
29
|
+
* **Étape A+B** : en-têtes constants pré-calculés au boot (Referrer/COOP/…). Pour le
|
|
30
|
+
* CSP, deux régimes mutuellement exclusifs : **statique** (posé tel quel via `headers`,
|
|
31
|
+
* 0 alloc/req) OU **nonce par requête** (`cspNonces` + placeholder `{{nonce}}` dans le
|
|
32
|
+
* CSP) → segments pré-split au boot, recomposés par requête via `cspFor(nonce)` (1
|
|
33
|
+
* `join`, aucun parse/regex dans le hot-path). Le nonce lui-même vit sur `Context`.
|
|
34
|
+
*
|
|
35
|
+
* @see Fetch Metadata · W3C CSP Level 3 · WHATWG (COOP/COEP/CORP).
|
|
36
|
+
*/
|
|
37
|
+
export declare class SecurityHeaders {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(o: ISecurityHeadersOptions);
|
|
40
|
+
/** Table d'en-têtes applicatifs CONSTANTS (figée), posée telle quelle par le firewall. */
|
|
41
|
+
get headers(): Readonly<Record<string, string>>;
|
|
42
|
+
/** `true` si le CSP est recomposé par requête avec un nonce (→ `cspFor`). */
|
|
43
|
+
get hasNonce(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Recompose le CSP en injectant le `nonce` de la requête aux emplacements
|
|
46
|
+
* `{{nonce}}`. 1 `join` (segments pré-split au boot). Le nonce est base64 (jamais
|
|
47
|
+
* `'`/`;`/espace) → aucune évasion possible du token CSP.
|
|
48
|
+
*
|
|
49
|
+
* @param nonce - nonce base64 de la requête (`Context.cspNonce`).
|
|
50
|
+
* @returns la valeur `Content-Security-Policy` à poser sur la réponse.
|
|
51
|
+
*/
|
|
52
|
+
cspFor(nonce: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Recompose le CSP en fusionnant ADDITIVEMENT les directives `extra` d'une
|
|
55
|
+
* route (`@Csp`) dans le CSP de base, PUIS en substituant le `nonce`. Le merge
|
|
56
|
+
* (parse/serialize) n'est payé QUE sur les routes décorées `@Csp` (rares) — le
|
|
57
|
+
* cas courant reste {@link cspFor} (1 `join`). Une directive déjà présente voit
|
|
58
|
+
* ses sources complétées (jamais dupliquée — W3C CSP3 §3).
|
|
59
|
+
*
|
|
60
|
+
* @param nonce - nonce base64 de la requête (ignoré si le CSP n'a pas de nonce).
|
|
61
|
+
* @param extra - directives additionnelles de la route (`directive → sources`).
|
|
62
|
+
* @returns la valeur `Content-Security-Policy` à poser sur la réponse.
|
|
63
|
+
*/
|
|
64
|
+
cspForExtra(nonce: string, extra: CspFragment): string;
|
|
65
|
+
}
|
|
66
|
+
export default SecurityHeaders;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import type * as Jose from "jose";
|
|
3
|
+
import type { IUser } from "@nodefony/user";
|
|
4
|
+
/**
|
|
5
|
+
* Réponse d'émission de jetons — nommage RFC 6749 §5.1 (snake_case, JSON).
|
|
6
|
+
* Le JWT part en `Authorization: Bearer`, JAMAIS en cookie ni en URL.
|
|
7
|
+
*/
|
|
8
|
+
export interface ITokenResponse {
|
|
9
|
+
access_token: string;
|
|
10
|
+
refresh_token: string;
|
|
11
|
+
token_type: "Bearer";
|
|
12
|
+
/** Durée de vie de l'access token (s). */
|
|
13
|
+
expires_in: number;
|
|
14
|
+
/** Scopes accordés (séparés par des espaces). */
|
|
15
|
+
scope: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Orchestrateur des jetons longue durée (P6 J4) — émission/refresh des JWT +
|
|
19
|
+
* **maintenance du store** (le seam `ITokenStore.gc()` n'a pas d'autre appelant).
|
|
20
|
+
*
|
|
21
|
+
* Au boot (si `jwt.enabled`) : résout le store pluggable (`tokenStore.store`),
|
|
22
|
+
* crée le keystore Ed25519, les pose au container (`tokenStore`/`jwtKeystore`,
|
|
23
|
+
* consommés par le `JwtAuthenticator` et les endpoints framework), puis arme un
|
|
24
|
+
* **timer de gc** `unref` (n'empêche pas l'arrêt) avec **jitter** de phase
|
|
25
|
+
* (étale les balayages entre process d'un cluster sur un store partagé). À
|
|
26
|
+
* l'arrêt (`onTerminate`) : `clearInterval`/`clearTimeout`.
|
|
27
|
+
*
|
|
28
|
+
* Émission = « password grant » M2M/CLI : credential vérifié par le service
|
|
29
|
+
* `users` → access (JWT signé, 15 min) + refresh (secret opaque haute entropie,
|
|
30
|
+
* stocké **haché**). Refresh = rotation + détection de rejeu (RFC 9700 §4.14).
|
|
31
|
+
*/
|
|
32
|
+
declare class TokenService extends Service {
|
|
33
|
+
#private;
|
|
34
|
+
module: Module;
|
|
35
|
+
constructor(module: Module);
|
|
36
|
+
/** `true` si l'émission JWT (signature + refresh) est opérationnelle. */
|
|
37
|
+
isEnabled(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Émetteur sous lequel cette application accepte d'être DÉCOUVERTE, ou `null`.
|
|
40
|
+
*
|
|
41
|
+
* C'est la question que pose `@nodefony/framework` au moment de monter (ou
|
|
42
|
+
* non) `/.well-known/oauth-authorization-server` et `/.well-known/jwks.json` :
|
|
43
|
+
* il ne lit pas la configuration de sécurité, il obtient une réponse. `null`
|
|
44
|
+
* = aucune route, donc `404` — pas de document creux, pas de demi-mesure.
|
|
45
|
+
*
|
|
46
|
+
* @returns l'émetteur canonique publiable, ou `null` si rien ne doit l'être
|
|
47
|
+
*/
|
|
48
|
+
publishedIssuer(): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Jeu de clés **publiques** de signature, tel qu'il doit être servi.
|
|
51
|
+
*
|
|
52
|
+
* Ne contient que des paramètres publics (RFC 8037/7517) — le keystore ne
|
|
53
|
+
* sérialise jamais `d`. Rien n'est calculé ici : la route est une porte, la
|
|
54
|
+
* matière vient du keystore.
|
|
55
|
+
*
|
|
56
|
+
* @returns le JWKS public
|
|
57
|
+
* @throws Error si la capacité JWT n'est pas active (garde de programmation :
|
|
58
|
+
* les routes ne sont montées que si {@link publishedIssuer} répond)
|
|
59
|
+
*/
|
|
60
|
+
getPublicJWKS(): Promise<Jose.JSONWebKeySet>;
|
|
61
|
+
/**
|
|
62
|
+
* Une passe de purge du store (`ITokenStore.gc()`) — point d'entrée public d'un
|
|
63
|
+
* ordonnanceur : le {@link GcScheduler} l'appelle, mais le futur worker cron
|
|
64
|
+
* (`security:token-gc` / k8s CronJob) peut l'appeler à sa place (poser alors
|
|
65
|
+
* `tokenStore.gcIntervalS: 0`). L'anti-empilement et la capture d'erreur vivent
|
|
66
|
+
* dans le GcScheduler (via `onError`) — ici, la passe métier nue.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Un store **local** (`memory`/`file`) est par-process (mémoires disjointes) :
|
|
69
|
+
* seul SON process peut le purger → le timer in-process reste indispensable. Un
|
|
70
|
+
* store **partagé** (ORM) peut être délégué au worker cron (un seul balayage).
|
|
71
|
+
*
|
|
72
|
+
* @returns nombre d'entrées purgées.
|
|
73
|
+
*/
|
|
74
|
+
runGc(): Promise<number>;
|
|
75
|
+
/**
|
|
76
|
+
* Émet un couple access/refresh après vérification d'un credential
|
|
77
|
+
* identifiant/mot de passe (grant M2M/CLI). Throttling NIST partagé si activé.
|
|
78
|
+
*
|
|
79
|
+
* @throws ThrottledError (429) — backoff actif.
|
|
80
|
+
* @throws AuthenticationError (401, message uniforme) — credential invalide.
|
|
81
|
+
*/
|
|
82
|
+
issueForCredentials(identifier: unknown, password: unknown, requestedScopes?: string[], resource?: unknown): Promise<ITokenResponse>;
|
|
83
|
+
issueTokens(user: IUser, requestedScopes?: string[], resource?: unknown, accessTtlS?: number): Promise<ITokenResponse>;
|
|
84
|
+
/**
|
|
85
|
+
* Rotation d'un refresh token (RFC 9700 §4.14) : valide le refresh présenté,
|
|
86
|
+
* émet un nouveau couple, révoque l'ancien. Un refresh **déjà révoqué**
|
|
87
|
+
* re-présenté = rejeu → toute la famille est coupée.
|
|
88
|
+
*
|
|
89
|
+
* @param rawRefresh - le refresh token présenté, en clair
|
|
90
|
+
* @param resource - ressource visée (RFC 8707 §2.2). Sur un `refresh_token`,
|
|
91
|
+
* la politique « may limit the acceptable resources to those that
|
|
92
|
+
* were originally granted […] or a subset thereof » : un jeton ne
|
|
93
|
+
* portant qu'une seule audience, le seul sous-ensemble possible est
|
|
94
|
+
* elle-même. Demander autre chose est donc refusé, jamais ignoré —
|
|
95
|
+
* sinon la rotation devient le chemin par lequel on obtient une
|
|
96
|
+
* audience qu'on n'a pas su demander à l'émission.
|
|
97
|
+
* @throws AuthenticationError (401) — refresh inconnu/expiré/révoqué, sujet banni.
|
|
98
|
+
* @throws InvalidTargetError (400) — `resource` demandée ≠ celle accordée
|
|
99
|
+
*/
|
|
100
|
+
refresh(rawRefresh: unknown, resource?: unknown): Promise<ITokenResponse>;
|
|
101
|
+
}
|
|
102
|
+
export default TokenService;
|
|
103
|
+
export { TokenService };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import type { IPage } from "nodefony";
|
|
3
|
+
import type { ITotpEnrollmentSummary, ITotpListQuery } from "../contracts/ITotpSecretStore.js";
|
|
4
|
+
import { type ITotpEnrollment, type ITotpActivation, type ITotpStatus, type ITotpLoginResult } from "../src/totp/totpOperations.js";
|
|
5
|
+
/**
|
|
6
|
+
* **2FA TOTP** (P6.17, RFC 6238) — service d'orchestration du second facteur.
|
|
7
|
+
*
|
|
8
|
+
* Coquille fine : au boot (si `totp.enabled`) il résout le **store** de secrets
|
|
9
|
+
* pluggable + la **clé de chiffrement** AES-256-GCM, puis délègue toute la logique
|
|
10
|
+
* aux opérations pures `totpOperations` (testées sans serveur). Le TOTP est un
|
|
11
|
+
* facteur de **login step-up** (le code n'est présenté qu'à la connexion, calque
|
|
12
|
+
* WebAuthn/OAuth), pas un authenticator du firewall — `session.user` n'est posé
|
|
13
|
+
* qu'une fois le second facteur validé (Zero Trust 401 protège tout le reste).
|
|
14
|
+
*
|
|
15
|
+
* **Clé de chiffrement** : le secret TOTP est réversible (le serveur le relit pour
|
|
16
|
+
* calculer le code) → chiffré, jamais haché. La clé vient de `totp.encryptionKey`
|
|
17
|
+
* (dérivée HKDF). Absente : en dev une clé **éphémère** est générée + WARNING (les
|
|
18
|
+
* secrets ne survivent pas au redémarrage) ; en **production** c'est fatal — 2FA
|
|
19
|
+
* désactivé (une clé éphémère rendrait les secrets illisibles après redémarrage /
|
|
20
|
+
* sur les autres pods). Politique calquée sur RedisIdempotencyStore.
|
|
21
|
+
*/
|
|
22
|
+
declare class TotpService extends Service {
|
|
23
|
+
#private;
|
|
24
|
+
module: Module;
|
|
25
|
+
constructor(module: Module);
|
|
26
|
+
/** `true` si le 2FA est opérationnel (activé en config, boot OK). */
|
|
27
|
+
isEnabled(): boolean;
|
|
28
|
+
/** Démarre l'enrôlement (secret + QR affichés 1×). */
|
|
29
|
+
beginEnrollment(userId: string, account: string): Promise<ITotpEnrollment>;
|
|
30
|
+
/** Confirme l'enrôlement par un 1ᵉʳ code → active + codes de récupération clairs. */
|
|
31
|
+
confirmEnrollment(userId: string, code: string): Promise<ITotpActivation>;
|
|
32
|
+
/** Vérifie un second facteur au login (code TOTP ou code de récupération). */
|
|
33
|
+
verifyLogin(userId: string, code: string): Promise<ITotpLoginResult>;
|
|
34
|
+
/** Désactive le 2FA d'un utilisateur. */
|
|
35
|
+
disable(userId: string): Promise<void>;
|
|
36
|
+
/** État 2FA d'un utilisateur (absent / pending / activé + codes restants). */
|
|
37
|
+
status(userId: string): Promise<ITotpStatus>;
|
|
38
|
+
/**
|
|
39
|
+
* Page d'enrôlements 2FA — pagination **native au store** (jamais un parcours
|
|
40
|
+
* complet). Vue sans secret ni condensats, garantie par le contrat du store.
|
|
41
|
+
*
|
|
42
|
+
* @param query - fenêtre + filtres ({@link ITotpListQuery}).
|
|
43
|
+
* @returns la page d'enrôlements.
|
|
44
|
+
*/
|
|
45
|
+
listPage(query: ITotpListQuery): Promise<IPage<ITotpEnrollmentSummary>>;
|
|
46
|
+
/**
|
|
47
|
+
* Nombre d'enrôlements correspondant aux filtres — le KPI « couverture 2FA »
|
|
48
|
+
* sans énumérer.
|
|
49
|
+
*
|
|
50
|
+
* @param query - filtres ({@link ITotpListQuery}) ; `limit` ignoré.
|
|
51
|
+
* @returns le compte exact.
|
|
52
|
+
*/
|
|
53
|
+
countEnrollments(query: ITotpListQuery): Promise<number>;
|
|
54
|
+
/** 2FA activé pour cet utilisateur ? (raccourci pour le flow de login). */
|
|
55
|
+
isEnabledFor(userId: string): Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
export default TotpService;
|
|
58
|
+
export { TotpService };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import type { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON } from "@simplewebauthn/server";
|
|
3
|
+
import type { IPage } from "nodefony";
|
|
4
|
+
import type { IWebAuthnCredential } from "../contracts/IWebAuthnCredential.js";
|
|
5
|
+
import type { IWebAuthnCredentialSummary, IWebAuthnListQuery } from "../contracts/IWebAuthnCredentialStore.js";
|
|
6
|
+
/** Sujet d'une cérémonie d'enregistrement — l'utilisateur qui crée un passkey. */
|
|
7
|
+
export interface IWebAuthnUser {
|
|
8
|
+
/** Identifiant applicatif stable (sub / username) — devient le `userHandle`. */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** Nom de compte affiché par l'OS (email, identifiant…). */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Nom complet optionnel (UX de l'invite système). */
|
|
13
|
+
readonly displayName?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Résultat d'une authentification WebAuthn vérifiée. */
|
|
16
|
+
export interface IWebAuthnAssertionResult {
|
|
17
|
+
/** Le credential résolu (état déjà mis à jour : compteur, sauvegarde, usage). */
|
|
18
|
+
readonly credential: IWebAuthnCredential;
|
|
19
|
+
/** Identifiant de l'utilisateur propriétaire du credential. */
|
|
20
|
+
readonly userId: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* **WebAuthn / passkeys** (P6 J9) — orchestrateur des deux cérémonies FIDO2
|
|
24
|
+
* (WebAuthn L3 §7.1 enregistrement, §7.2 authentification).
|
|
25
|
+
*
|
|
26
|
+
* MFA **phishing-resistant** : la clé privée ne quitte jamais l'authenticator
|
|
27
|
+
* (Touch ID, Windows Hello, clé FIDO). Le serveur ne manipule QUE des clés
|
|
28
|
+
* publiques + vérifie des signatures. La vérification cryptographique (parsing
|
|
29
|
+
* CBOR/COSE, signatures ES256/RS256/EdDSA) est déléguée à `@simplewebauthn/server`
|
|
30
|
+
* (lib auditée de l'écosystème), **importée paresseusement** au 1ᵉʳ usage (cold
|
|
31
|
+
* path — l'enregistrement/login n'est pas le hot path par requête).
|
|
32
|
+
*
|
|
33
|
+
* Au boot (si `passkeys.enabled`) : résout le RP (rpID/rpName/origines depuis la
|
|
34
|
+
* config, sinon le domaine de l'app) + le store de credentials pluggable
|
|
35
|
+
* (`webAuthnCredentialStore` du container, sinon le builtin mémoire) et le pose
|
|
36
|
+
* au container — une application peut donc fournir le sien avant le boot.
|
|
37
|
+
*
|
|
38
|
+
* Il n'existe **pas** d'authenticator passkey : `WebAuthnController`
|
|
39
|
+
* (`@nodefony/framework`) mène les deux cérémonies via ce service, puis
|
|
40
|
+
* `AuthFlow.establishSessionFor()` ouvre la session BFF ; les requêtes
|
|
41
|
+
* suivantes sont ré-authentifiées par `SessionAuthenticator`.
|
|
42
|
+
*
|
|
43
|
+
* **Anti-rejeu** : le challenge serveur est porté HORS de ce service (en session
|
|
44
|
+
* BFF par le controller) ; chaque `verify*` reçoit le `expectedChallenge` qu'il
|
|
45
|
+
* a émis — un challenge n'est jamais réutilisable.
|
|
46
|
+
*/
|
|
47
|
+
declare class WebAuthnService extends Service {
|
|
48
|
+
#private;
|
|
49
|
+
module: Module;
|
|
50
|
+
constructor(module: Module);
|
|
51
|
+
/** `true` si les cérémonies sont opérationnelles (passkeys activés, boot OK). */
|
|
52
|
+
isEnabled(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Prépare les options de `navigator.credentials.create()` — le défi à signer +
|
|
55
|
+
* les contraintes (RP, type d'attestation, sélection d'authenticator). Le
|
|
56
|
+
* challenge renvoyé doit être stocké côté serveur (session) par l'appelant.
|
|
57
|
+
*
|
|
58
|
+
* `excludeCredentials` liste les passkeys déjà enregistrés du même utilisateur
|
|
59
|
+
* pour empêcher un double enregistrement sur le même authenticator (§7.1).
|
|
60
|
+
*/
|
|
61
|
+
generateRegistrationOptions(user: IWebAuthnUser): Promise<PublicKeyCredentialCreationOptionsJSON>;
|
|
62
|
+
/**
|
|
63
|
+
* Vérifie la réponse d'enregistrement (challenge, origine, rpIdHash, flags,
|
|
64
|
+
* format d'attestation) et **persiste** le nouveau credential.
|
|
65
|
+
*
|
|
66
|
+
* @param expectedChallenge - le challenge émis par {@link generateRegistrationOptions} (session).
|
|
67
|
+
* @param userId - propriétaire du credential (utilisateur authentifié/en création).
|
|
68
|
+
* @param requestOrigin - origine HTTP de la requête (validée si aucune origine n'est configurée).
|
|
69
|
+
* @throws AuthenticationError (401) — vérification échouée.
|
|
70
|
+
* @throws WebAuthnError (409) — plafond `passkeys.maxPerUser` atteint.
|
|
71
|
+
*/
|
|
72
|
+
verifyRegistration(response: RegistrationResponseJSON, expectedChallenge: string, userId: string, requestOrigin?: string): Promise<IWebAuthnCredential>;
|
|
73
|
+
/**
|
|
74
|
+
* Prépare les options de `navigator.credentials.get()`. Sans `userId`
|
|
75
|
+
* (usernameless) : `allowCredentials` est omis → l'authenticator propose ses
|
|
76
|
+
* passkeys découvrables (UX cible). Avec `userId` : ciblage des credentials
|
|
77
|
+
* connus de cet utilisateur.
|
|
78
|
+
*
|
|
79
|
+
* @param userId - identité **déjà prouvée** (session en cours) et jamais un
|
|
80
|
+
* identifiant reçu d'un appelant non authentifié : `allowCredentials`
|
|
81
|
+
* révélerait alors qu'un compte porte une passkey et **lesquelles**, or un
|
|
82
|
+
* `credentialId` est corrélable entre sites (W3C WebAuthn L3, « Privacy leak
|
|
83
|
+
* via credential IDs »). Le controller BFF applique cette règle —
|
|
84
|
+
* `WebAuthnController.loginOptions()` ne cible que depuis la session.
|
|
85
|
+
*/
|
|
86
|
+
generateAuthenticationOptions(userId?: string): Promise<PublicKeyCredentialRequestOptionsJSON>;
|
|
87
|
+
/**
|
|
88
|
+
* Vérifie une assertion (signature sur `authData ‖ SHA-256(clientDataJSON)`
|
|
89
|
+
* avec la clé publique stockée, §7.2) + applique l'état (compteur anti-clone,
|
|
90
|
+
* sauvegarde, usage). Résout l'utilisateur propriétaire via le credentialId.
|
|
91
|
+
*
|
|
92
|
+
* @param expectedChallenge - le challenge émis par {@link generateAuthenticationOptions} (session).
|
|
93
|
+
* @throws AuthenticationError (401) — credential inconnu ou vérification échouée.
|
|
94
|
+
*/
|
|
95
|
+
verifyAuthentication(response: AuthenticationResponseJSON, expectedChallenge: string, requestOrigin?: string): Promise<IWebAuthnAssertionResult>;
|
|
96
|
+
/** Liste les credentials d'un utilisateur (UX « mes appareils »). */
|
|
97
|
+
listUserCredentials(userId: string): Promise<IWebAuthnCredential[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Page de passkeys pour le data plane admin (vue TRANSVERSE : « quels appareils
|
|
100
|
+
* portent des passkeys sur toute la plateforme »).
|
|
101
|
+
*
|
|
102
|
+
* ≠ {@link listUserCredentials}, qui sert la fiche d'UN utilisateur et le chemin
|
|
103
|
+
* chaud du login. Ici on ne matérialise jamais plus d'une page, et la projection
|
|
104
|
+
* du store exclut la clé publique.
|
|
105
|
+
*/
|
|
106
|
+
listCredentialsPage(query: IWebAuthnListQuery): Promise<IPage<IWebAuthnCredentialSummary>>;
|
|
107
|
+
/**
|
|
108
|
+
* Nombre de passkeys correspondant aux filtres, ou `-1` si le backend ne sait
|
|
109
|
+
* pas compter à coût raisonnable (Redis).
|
|
110
|
+
*/
|
|
111
|
+
countCredentials(query: IWebAuthnListQuery): Promise<number>;
|
|
112
|
+
/** Révoque un credential (retrait d'un appareil). */
|
|
113
|
+
removeCredential(credentialId: string): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Supprime un credential **du propriétaire** (self-service, anti-IDOR) : la
|
|
116
|
+
* suppression n'aboutit que si le credential appartient bien à `userId`, sinon
|
|
117
|
+
* `false` — 404 indiscernable côté client (on ne révèle pas l'existence d'un
|
|
118
|
+
* credential d'autrui).
|
|
119
|
+
*/
|
|
120
|
+
removeUserCredential(userId: string, credentialId: string): Promise<boolean>;
|
|
121
|
+
}
|
|
122
|
+
export default WebAuthnService;
|
|
123
|
+
export { WebAuthnService };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import { type IWebhookCounts } from "../src/webhook/webhookFilters.js";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import type { IPage } from "nodefony";
|
|
5
|
+
import type { IWebhookListQuery } from "../contracts/IWebhookStore.js";
|
|
6
|
+
import type { IWebhookEndpoint, IWebhookDelivery, WebhookEndpointSummary, WebhookEndpointUpdate } from "../contracts/IWebhookEndpoint.js";
|
|
7
|
+
import { type IDeliveryResult } from "../src/webhook/webhookDelivery.js";
|
|
8
|
+
/** Entrée de création d'un endpoint (les champs système sont dérivés). */
|
|
9
|
+
export interface IWebhookRegisterInput {
|
|
10
|
+
/** URL de destination (validée anti-SSRF). */
|
|
11
|
+
readonly url: string;
|
|
12
|
+
/** Actions d'audit souscrites (`"*"` = toutes). */
|
|
13
|
+
readonly events: readonly string[];
|
|
14
|
+
/** Libellé humain optionnel. */
|
|
15
|
+
readonly description?: string | null;
|
|
16
|
+
/** Actif dès la création ? Défaut : `true`. */
|
|
17
|
+
readonly enabled?: boolean;
|
|
18
|
+
/** Identité de l'admin créateur (traçabilité). */
|
|
19
|
+
readonly createdBy?: string | null;
|
|
20
|
+
/** Slot multi-tenant (réservé). */
|
|
21
|
+
readonly tenantId?: string | null;
|
|
22
|
+
/** Métadonnées extensibles. */
|
|
23
|
+
readonly metadata?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** Résultat de création/rotation : le secret en clair n'est exposé qu'ici, **une fois**. */
|
|
26
|
+
export interface IWebhookSecretReveal {
|
|
27
|
+
/** Endpoint (sans secret chiffré). */
|
|
28
|
+
readonly endpoint: WebhookEndpointSummary;
|
|
29
|
+
/** Secret de signature en clair (`whsec_…`) — à communiquer au consommateur. */
|
|
30
|
+
readonly secret: string;
|
|
31
|
+
}
|
|
32
|
+
/** Politique de livraison lue par le dispatcher (Slice B). */
|
|
33
|
+
export interface IWebhookDeliveryPolicy {
|
|
34
|
+
readonly timestampToleranceS: number;
|
|
35
|
+
readonly maxRetries: number;
|
|
36
|
+
readonly autoDisableThreshold: number;
|
|
37
|
+
readonly deliveryTimeoutMs: number;
|
|
38
|
+
readonly maxConcurrent: number;
|
|
39
|
+
readonly maxQueue: number;
|
|
40
|
+
readonly allowHttp: boolean;
|
|
41
|
+
readonly denyPrivateIps: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* **Webhooks sortants** (P6.13) — service d'orchestration du registre d'endpoints.
|
|
45
|
+
*
|
|
46
|
+
* Coquille fine : au boot (si `webhooks.enabled`) il résout le **store**
|
|
47
|
+
* d'endpoints pluggable + la **clé de chiffrement** des secrets de signature, puis
|
|
48
|
+
* expose le CRUD (register/list/update/rotate/revoke). Le secret de signature est
|
|
49
|
+
* **chiffré au repos** (réversible : relu pour signer chaque livraison, jamais
|
|
50
|
+
* haché). La livraison signée elle-même (Standard Webhooks v1) vit dans le
|
|
51
|
+
* dispatcher (Slice B), qui consomme {@link getSnapshot}/{@link getSigningKey}.
|
|
52
|
+
*
|
|
53
|
+
* Toute URL est validée **anti-SSRF** à l'enregistrement (et re-pinnée à la
|
|
54
|
+
* livraison). Politique de clé calquée sur TOTP/RedisIdempotencyStore : absente en
|
|
55
|
+
* dev = clé éphémère + WARNING ; en production = fatal (webhooks désactivés).
|
|
56
|
+
*/
|
|
57
|
+
declare class WebhookService extends Service {
|
|
58
|
+
#private;
|
|
59
|
+
module: Module;
|
|
60
|
+
constructor(module: Module);
|
|
61
|
+
/** Le service est-il opérationnel (activé + store + clé) ? */
|
|
62
|
+
isReady(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Champs de tri que le backend **actuellement branché** sait honorer, en
|
|
65
|
+
* vocabulaire public. Le data plane admin les passe en allowlist au traducteur
|
|
66
|
+
* de requête de page : hors de cette liste, un `?order=` est refusé en 400.
|
|
67
|
+
*
|
|
68
|
+
* La liste vient du store, jamais d'une constante recopiée ici : un backend
|
|
69
|
+
* qui ne trierait pas refuserait alors le tri **sans qu'aucune règle
|
|
70
|
+
* supplémentaire ne soit écrite**. Store absent (webhooks désactivés) ⇒ aucune
|
|
71
|
+
* capacité annoncée, donc aucun tri promis.
|
|
72
|
+
*
|
|
73
|
+
* @returns les champs triables, ou un tableau vide.
|
|
74
|
+
*/
|
|
75
|
+
sortableFields(): readonly string[];
|
|
76
|
+
/**
|
|
77
|
+
* Enregistre un endpoint : valide l'URL (anti-SSRF), génère un secret de
|
|
78
|
+
* signature, le chiffre au repos. Retourne l'endpoint + le secret **en clair**
|
|
79
|
+
* (la seule occasion de le lire pour le copier).
|
|
80
|
+
*
|
|
81
|
+
* @throws SsrfError si l'URL est invalide / cible non publique.
|
|
82
|
+
*/
|
|
83
|
+
register(input: IWebhookRegisterInput): Promise<IWebhookSecretReveal>;
|
|
84
|
+
/**
|
|
85
|
+
* Page d'endpoints (vue publique, sans secret) — pagination **native au
|
|
86
|
+
* store** : la console n'a jamais tout le registre en RAM.
|
|
87
|
+
*
|
|
88
|
+
* @param query - fenêtre + filtres ({@link IWebhookListQuery}).
|
|
89
|
+
* @returns la page, chaque endpoint réduit à sa vue publique.
|
|
90
|
+
*/
|
|
91
|
+
listPage(query: IWebhookListQuery): Promise<IPage<WebhookEndpointSummary>>;
|
|
92
|
+
/**
|
|
93
|
+
* Nombre d'endpoints correspondant aux filtres (`COUNT` natif au store).
|
|
94
|
+
*
|
|
95
|
+
* @param query - filtres ({@link IWebhookListQuery}) ; `limit` ignoré.
|
|
96
|
+
* @returns le compte exact, ou `-1` si le backend ne sait pas compter.
|
|
97
|
+
*/
|
|
98
|
+
countEndpoints(query: IWebhookListQuery): Promise<number>;
|
|
99
|
+
/**
|
|
100
|
+
* Les compteurs de tête de la console — posés sur la collection ENTIÈRE, pas
|
|
101
|
+
* sur la page affichée.
|
|
102
|
+
*
|
|
103
|
+
* Un endpoint peut être **actif ET en échec** : les facettes se recoupent, et
|
|
104
|
+
* aucune n'est déduite d'une autre par soustraction. Chaque compteur vaut
|
|
105
|
+
* `null` si le backend ne sait pas compter.
|
|
106
|
+
*
|
|
107
|
+
* @param query - filtres à appliquer avant comptage (sans fenêtre).
|
|
108
|
+
*/
|
|
109
|
+
countWebhookFacets(query?: Partial<IWebhookListQuery>): Promise<IWebhookCounts>;
|
|
110
|
+
/** Un endpoint par id (vue publique), ou `null`. */
|
|
111
|
+
getEndpoint(id: string): Promise<WebhookEndpointSummary | null>;
|
|
112
|
+
/**
|
|
113
|
+
* Met à jour les champs mutables (url/events/enabled/description/metadata).
|
|
114
|
+
* Une nouvelle `url` est re-validée anti-SSRF. Retourne l'endpoint mis à jour,
|
|
115
|
+
* ou `null` si absent.
|
|
116
|
+
*/
|
|
117
|
+
update(id: string, patch: Pick<WebhookEndpointUpdate, "url" | "events" | "enabled" | "description" | "metadata">): Promise<WebhookEndpointSummary | null>;
|
|
118
|
+
/** Active/désactive un endpoint (révocation douce = `false`). */
|
|
119
|
+
setEnabled(id: string, enabled: boolean): Promise<WebhookEndpointSummary | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Régénère le secret de signature (rotation) et retourne le nouveau en clair.
|
|
122
|
+
* L'ancien cesse immédiatement d'être valide. `null` si l'endpoint est absent.
|
|
123
|
+
*/
|
|
124
|
+
rotateSecret(id: string): Promise<IWebhookSecretReveal | null>;
|
|
125
|
+
/**
|
|
126
|
+
* Révèle le secret en clair d'un endpoint (réversible — usage admin, à auditer
|
|
127
|
+
* par l'appelant). `null` si absent.
|
|
128
|
+
*/
|
|
129
|
+
revealSecret(id: string): Promise<string | null>;
|
|
130
|
+
/** Supprime un endpoint. Retourne `false` si absent. */
|
|
131
|
+
delete(id: string): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Historique des dernières livraisons d'un endpoint (plus récentes d'abord) —
|
|
134
|
+
* ce que Nodefony a ENVOYÉ + la réponse observée. RAM, borné, par pod
|
|
135
|
+
* (observabilité éphémère, non persistée). `[]` si aucune livraison.
|
|
136
|
+
*/
|
|
137
|
+
listDeliveries(id: string): IWebhookDelivery[];
|
|
138
|
+
/** Snapshot mémoire (sync) des endpoints — itération du dispatcher (si >0). */
|
|
139
|
+
getSnapshot(): IWebhookEndpoint[];
|
|
140
|
+
/**
|
|
141
|
+
* Nombre d'endpoints (0-alloc) — court-circuit hot-path du dispatcher.
|
|
142
|
+
*
|
|
143
|
+
* @remarks C'est ICI que la fraîcheur se joue, pas seulement dans
|
|
144
|
+
* {@link getSnapshot} : un pod démarré avant toute création de webhook a un
|
|
145
|
+
* cache VIDE, court-circuite sur ce zéro et n'atteindrait jamais le snapshot.
|
|
146
|
+
*/
|
|
147
|
+
endpointCount(): number;
|
|
148
|
+
/** Déchiffre le secret de signature d'un endpoint (pour signer une livraison). */
|
|
149
|
+
decryptEndpointSecret(endpoint: IWebhookEndpoint): Buffer;
|
|
150
|
+
/** Politique de livraison (tolérance/retries/timeout…) issue de la config. */
|
|
151
|
+
getDeliveryPolicy(): IWebhookDeliveryPolicy;
|
|
152
|
+
/**
|
|
153
|
+
* Enregistre le résultat d'une livraison (appelé par le dispatcher) :
|
|
154
|
+
* lastDelivery*, compteur d'échecs consécutifs, et **auto-désactivation** de
|
|
155
|
+
* l'endpoint au-delà du seuil (façon GitHub). Le succès remet le compteur à 0.
|
|
156
|
+
*/
|
|
157
|
+
markDelivery(id: string, result: IDeliveryResult): Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
export { WebhookService };
|
|
160
|
+
export default WebhookService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Résout la hiérarchie de rôles — `ROLE_ADMIN` hérite `ROLE_USER`, etc.
|
|
3
|
+
*
|
|
4
|
+
* Aplatissement DFS **précalculé au boot** (lecture O(1) au runtime, hot-path) +
|
|
5
|
+
* **détection de cycles au boot** (throw avec le chemin complet, pas de fail-silent).
|
|
6
|
+
* Niveau A de l'autorisation (P6.8).
|
|
7
|
+
*/
|
|
8
|
+
export declare class RoleHierarchyWalker {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(hierarchy?: Record<string, readonly string[]>);
|
|
11
|
+
/**
|
|
12
|
+
* L'utilisateur (rôles plats) possède-t-il le rôle requis, hiérarchie résolue ?
|
|
13
|
+
*
|
|
14
|
+
* @param userRoles - rôles plats de l'utilisateur.
|
|
15
|
+
* @param required - rôle exigé.
|
|
16
|
+
*/
|
|
17
|
+
hasRole(userRoles: readonly string[], required: string): boolean;
|
|
18
|
+
/** Ensemble complet des rôles atteignables (plats + hérités). */
|
|
19
|
+
reachableRoles(userRoles: readonly string[]): Set<string>;
|
|
20
|
+
}
|
|
21
|
+
export default RoleHierarchyWalker;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ContextType } from "@nodefony/http";
|
|
2
|
+
import type { ISecuredArea } from "../contracts/ISecuredArea.js";
|
|
3
|
+
import type { ISecurityAreaConfig } from "../config/defineModuleConfig.js";
|
|
4
|
+
/**
|
|
5
|
+
* Zone sécurisée concrète — pattern d'URL compilé + métadonnées d'authentification.
|
|
6
|
+
*
|
|
7
|
+
* Objet **léger** (pas un Service DI : zéro besoin d'event/log par zone, hot-path).
|
|
8
|
+
* Le firewall en instancie une par entrée `areas` de la config, triées par
|
|
9
|
+
* spécificité au boot.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SecuredArea implements ISecuredArea {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly pattern: RegExp;
|
|
14
|
+
readonly security: boolean;
|
|
15
|
+
readonly stateless: boolean;
|
|
16
|
+
readonly mode: "first" | "all";
|
|
17
|
+
readonly authenticators: readonly string[];
|
|
18
|
+
readonly host?: string;
|
|
19
|
+
readonly realtime: boolean;
|
|
20
|
+
readonly resource?: string;
|
|
21
|
+
constructor(name: string, config: ISecurityAreaConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Cœur du match — pathname (+ host éventuel) déjà extraits, SANS `context`.
|
|
24
|
+
* Réutilisable par le verrou WebSocket (une frame n'a qu'un path) : source
|
|
25
|
+
* UNIQUE de la décision de zone (invariant `api.request {path}` ≤ `GET {path}`).
|
|
26
|
+
*/
|
|
27
|
+
matchPath(pathname: string, host?: string): boolean;
|
|
28
|
+
/** La requête tombe-t-elle dans cette zone ? (host éventuel + pathname). */
|
|
29
|
+
match(context: ContextType): boolean;
|
|
30
|
+
}
|
|
31
|
+
export default SecuredArea;
|