@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,41 @@
|
|
|
1
|
+
import type { TotpAlgorithm } from "../src/totp/totpCrypto.js";
|
|
2
|
+
/**
|
|
3
|
+
* Secret TOTP d'un utilisateur (2FA) — **un seul par utilisateur** (clé = `userId`).
|
|
4
|
+
*
|
|
5
|
+
* Le secret partagé `K` est **chiffré au repos** ({@link ITotpSecret.secretEnc}) :
|
|
6
|
+
* le serveur doit pouvoir le **relire** pour recalculer le code à chaque login →
|
|
7
|
+
* réversible, donc chiffré (AES-256-GCM), **jamais** haché (≠ mot de passe / clé
|
|
8
|
+
* API). Les codes de récupération, eux, sont **hachés** (verify-only).
|
|
9
|
+
*/
|
|
10
|
+
export interface ITotpSecret {
|
|
11
|
+
/** Identifiant de l'utilisateur propriétaire (clé naturelle). */
|
|
12
|
+
readonly userId: string;
|
|
13
|
+
/**
|
|
14
|
+
* Secret partagé `K` **chiffré** (blob opaque : `iv.tag.ciphertext` base64url).
|
|
15
|
+
* Produit/lu par le service détenteur de la clé — le store ne voit que des octets.
|
|
16
|
+
*/
|
|
17
|
+
readonly secretEnc: string;
|
|
18
|
+
/** Fonction HMAC du code (RFC 6238 §1.2). */
|
|
19
|
+
readonly algorithm: TotpAlgorithm;
|
|
20
|
+
/** Nombre de chiffres du code. */
|
|
21
|
+
readonly digits: number;
|
|
22
|
+
/** Période d'un code en secondes. */
|
|
23
|
+
readonly period: number;
|
|
24
|
+
/** Condensats `sha256` des codes de récupération **non encore consommés**. */
|
|
25
|
+
recoveryCodes: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Horodatage de **confirmation** de l'enrôlement (epoch ms), ou `null` tant que
|
|
28
|
+
* l'utilisateur n'a pas prouvé qu'il lit bien les codes (anti-lock-out).
|
|
29
|
+
*/
|
|
30
|
+
confirmedAt: number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Dernière tranche temporelle `T` ayant validé un code (RFC 6238 §5.2) — un code
|
|
33
|
+
* déjà consommé dans sa fenêtre **ne doit pas resservir** (anti-rejeu).
|
|
34
|
+
*/
|
|
35
|
+
lastUsedStep: number | null;
|
|
36
|
+
/** Horodatage de création (epoch ms). */
|
|
37
|
+
readonly createdAt: number;
|
|
38
|
+
/** Horodatage du dernier usage réussi (epoch ms), ou `null`. */
|
|
39
|
+
lastUsedAt: number | null;
|
|
40
|
+
}
|
|
41
|
+
export default ITotpSecret;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { IPage, IPageQuery } from "nodefony";
|
|
2
|
+
import type { ITotpSecret } from "./ITotpSecret.js";
|
|
3
|
+
/**
|
|
4
|
+
* Vue d'un enrôlement 2FA pour l'INTROSPECTION admin (« qui a activé le 2FA,
|
|
5
|
+
* qui est resté en attente de confirmation »).
|
|
6
|
+
*
|
|
7
|
+
* **Sans secret, par construction du contrat** — ni `secretEnc` (le secret
|
|
8
|
+
* partagé, réversible : il permettrait de générer les codes de la victime), ni
|
|
9
|
+
* `recoveryCodes` (leurs condensats, matière à attaque hors ligne). La garantie
|
|
10
|
+
* porte sur ce qui SORT du store : quel que soit le backend, ces champs ne
|
|
11
|
+
* peuvent pas remonter par ce chemin, même si un appelant les demandait. Le
|
|
12
|
+
* NOMBRE de codes restants, lui, est exposé — c'est l'information
|
|
13
|
+
* d'exploitation (qui se verrouillera au prochain changement d'appareil).
|
|
14
|
+
*/
|
|
15
|
+
export interface ITotpEnrollmentSummary {
|
|
16
|
+
/** Utilisateur propriétaire. */
|
|
17
|
+
readonly userId: string;
|
|
18
|
+
/** Fonction HMAC du code (RFC 6238 §1.2). */
|
|
19
|
+
readonly algorithm: string;
|
|
20
|
+
/** Nombre de chiffres du code. */
|
|
21
|
+
readonly digits: number;
|
|
22
|
+
/** Période d'un code en secondes. */
|
|
23
|
+
readonly period: number;
|
|
24
|
+
/** Confirmation de l'enrôlement (epoch ms), ou `null` = en attente. */
|
|
25
|
+
readonly confirmedAt: number | null;
|
|
26
|
+
/** Création de l'enrôlement (epoch ms). */
|
|
27
|
+
readonly createdAt: number;
|
|
28
|
+
/** Dernier usage réussi (epoch ms), ou `null` = jamais servi. */
|
|
29
|
+
readonly lastUsedAt: number | null;
|
|
30
|
+
/** Nombre de codes de récupération NON consommés (jamais les condensats). */
|
|
31
|
+
readonly recoveryCodesLeft: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Requête de listing des enrôlements 2FA — {@link IPageQuery} + les filtres qui
|
|
35
|
+
* ont un sens ici. `q` (hérité) = **préfixe** d'`userId` (retrouver un id
|
|
36
|
+
* partiel collé depuis la console).
|
|
37
|
+
*/
|
|
38
|
+
export interface ITotpListQuery extends IPageQuery {
|
|
39
|
+
/**
|
|
40
|
+
* `true` = enrôlements confirmés seulement, `false` = en attente seulement
|
|
41
|
+
* (comptes à relancer : un secret jamais confirmé ne protège personne),
|
|
42
|
+
* omis = les deux.
|
|
43
|
+
*/
|
|
44
|
+
confirmed?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Champs mutables d'un secret TOTP (patch partiel). */
|
|
47
|
+
export interface TotpSecretUpdate {
|
|
48
|
+
/** Confirmation de l'enrôlement (epoch ms) — passe le secret en « actif ». */
|
|
49
|
+
confirmedAt?: number | null;
|
|
50
|
+
/** Stock résiduel de codes de récupération hachés (après consommation). */
|
|
51
|
+
recoveryCodes?: string[];
|
|
52
|
+
/** Dernière tranche `T` validée (anti-rejeu RFC 6238 §5.2). */
|
|
53
|
+
lastUsedStep?: number;
|
|
54
|
+
/** Horodatage du dernier usage réussi (epoch ms). */
|
|
55
|
+
lastUsedAt?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Store **pluggable** du secret TOTP par utilisateur — découple le cœur du backend
|
|
59
|
+
* de persistance (mémoire / fichier / ORM / Redis). Convention-frère
|
|
60
|
+
* d'`IWebAuthnCredentialStore` / `ITokenStore` : le builtin `memory` est sans
|
|
61
|
+
* dépendance, les adapters lourds s'enregistrent depuis leur propre module.
|
|
62
|
+
*
|
|
63
|
+
* Modèle **1 secret / utilisateur** (clé = `userId`) — `save` est un **upsert**.
|
|
64
|
+
*/
|
|
65
|
+
export interface ITotpSecretStore {
|
|
66
|
+
/** Secret TOTP de l'utilisateur, ou `null` si non enrôlé. */
|
|
67
|
+
findByUser(userId: string): Promise<ITotpSecret | null>;
|
|
68
|
+
/** Crée ou remplace le secret de l'utilisateur (upsert — ré-enrôlement). */
|
|
69
|
+
save(secret: ITotpSecret): Promise<void>;
|
|
70
|
+
/** Applique un patch partiel (confirmation, anti-rejeu, consommation de codes). */
|
|
71
|
+
update(userId: string, patch: TotpSecretUpdate): Promise<void>;
|
|
72
|
+
/** Supprime le secret de l'utilisateur (désactivation du 2FA). */
|
|
73
|
+
delete(userId: string): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Page d'enrôlements 2FA pour le data plane admin — ne matérialise jamais
|
|
76
|
+
* plus d'une page, filtres appliqués au store, **secrets exclus** (cf
|
|
77
|
+
* {@link ITotpEnrollmentSummary}).
|
|
78
|
+
*
|
|
79
|
+
* Ordre contractuel : `createdAt` DESC, départagé par `userId` ASC.
|
|
80
|
+
*/
|
|
81
|
+
listPage(query: ITotpListQuery): Promise<IPage<ITotpEnrollmentSummary>>;
|
|
82
|
+
/**
|
|
83
|
+
* Nombre d'enrôlements correspondant aux filtres (`COUNT` natif) — le KPI
|
|
84
|
+
* « couverture 2FA » sans énumérer.
|
|
85
|
+
*/
|
|
86
|
+
countEnrollments(query: ITotpListQuery): Promise<number>;
|
|
87
|
+
}
|
|
88
|
+
export default ITotpSecretStore;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enregistrement d'un **credential WebAuthn / passkey** côté serveur — le
|
|
3
|
+
* `credentialRecord` de WebAuthn L3 §7.1 (produit par la cérémonie
|
|
4
|
+
* d'enregistrement, relu et mis à jour à chaque authentification §7.2).
|
|
5
|
+
*
|
|
6
|
+
* Le serveur ne stocke QUE de la donnée publique : la clé privée ne quitte
|
|
7
|
+
* jamais l'authenticator (Touch ID, Windows Hello, clé FIDO…). Une fuite de ce
|
|
8
|
+
* store ne compromet aucun compte — une clé publique est inexploitable seule.
|
|
9
|
+
*/
|
|
10
|
+
export interface IWebAuthnCredential {
|
|
11
|
+
/** Identifiant du credential (base64url) — unique, fourni par l'authenticator. */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** Identifiant de l'utilisateur propriétaire (sub / userHandle applicatif). */
|
|
14
|
+
readonly userId: string;
|
|
15
|
+
/** Clé publique **COSE** encodée base64url — vérifie la signature des assertions. */
|
|
16
|
+
readonly publicKey: string;
|
|
17
|
+
/**
|
|
18
|
+
* Compteur de signatures (WebAuthn §6.1.1) — anti-clone : SHOULD croître à
|
|
19
|
+
* chaque authentification. `0` = authenticator sans compteur (les passkeys
|
|
20
|
+
* synchronisées iCloud/Google le laissent souvent à 0, ce n'est pas une erreur).
|
|
21
|
+
*/
|
|
22
|
+
signCount: number;
|
|
23
|
+
/** Transports annoncés (`usb` | `nfc` | `ble` | `internal` | `hybrid`). */
|
|
24
|
+
readonly transports: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* BE flag (Backup Eligibility, §6.1.3) — credential multi-appareils
|
|
27
|
+
* (synchronisable). Fixé à l'enregistrement, ne change **jamais**.
|
|
28
|
+
*/
|
|
29
|
+
readonly backupEligible: boolean;
|
|
30
|
+
/** BS flag (Backup State) — actuellement sauvegardé. Peut évoluer dans le temps. */
|
|
31
|
+
backupState: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* La cérémonie a-t-elle réalisé une **vérification d'utilisateur** (biométrie/
|
|
34
|
+
* PIN) au moins une fois (`uvInitialized`, §7.2) — base du step-up MFA.
|
|
35
|
+
*/
|
|
36
|
+
uvInitialized: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Surnom optionnel de la passkey (« MacBook de Chris »).
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ **Emplacement réservé — rien ne l'écrit aujourd'hui.** Le champ est porté
|
|
41
|
+
* par ce contrat, par les trois stores (memory, drizzle, redis) et par la vue
|
|
42
|
+
* admin de Studio, mais **aucune API publique ne le renseigne** : ni endpoint,
|
|
43
|
+
* ni setter. En pratique il vaut donc toujours `undefined`, et l'écran retombe
|
|
44
|
+
* sur son libellé de repli (`Passkey ···1234`).
|
|
45
|
+
*
|
|
46
|
+
* Le garder coûte zéro (il traverse déjà toute la chaîne) ; le renseigner
|
|
47
|
+
* demande une décision produit — un endpoint de renommage, avec la question de
|
|
48
|
+
* qui a le droit de renommer la passkey de qui.
|
|
49
|
+
*/
|
|
50
|
+
nickname?: string;
|
|
51
|
+
/** Création (epoch ms). */
|
|
52
|
+
readonly createdAt: number;
|
|
53
|
+
/** Dernière authentification réussie (epoch ms), ou `null` si jamais utilisé. */
|
|
54
|
+
lastUsedAt: number | null;
|
|
55
|
+
}
|
|
56
|
+
export default IWebAuthnCredential;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { IPage, IPageQuery } from "nodefony";
|
|
2
|
+
import type { IWebAuthnCredential } from "./IWebAuthnCredential.js";
|
|
3
|
+
/**
|
|
4
|
+
* Vue d'une passkey pour l'INTROSPECTION admin (« quels appareils portent des
|
|
5
|
+
* passkeys, lesquelles meurent avec leur appareil »).
|
|
6
|
+
*
|
|
7
|
+
* **Sans `publicKey`, par construction du contrat.** La clé publique n'est pas un
|
|
8
|
+
* secret — c'est sa nature d'être publique — mais elle n'a aucune valeur
|
|
9
|
+
* d'exploitation dans une console : c'est de la matière cryptographique brute que
|
|
10
|
+
* personne ne lit, et une projection minimale est plus facile à garder juste. Ce
|
|
11
|
+
* qui S'EXPLOITE est ici : `backupState` (une passkey non sauvegardée disparaît
|
|
12
|
+
* avec l'appareil → l'utilisateur se verrouille dehors) et `signCount` (le
|
|
13
|
+
* compteur anti-clone du §6.1.1).
|
|
14
|
+
*/
|
|
15
|
+
export interface IWebAuthnCredentialSummary {
|
|
16
|
+
/** Identifiant du credential (base64url) — la clé naturelle. */
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/** Utilisateur propriétaire. */
|
|
19
|
+
readonly userId: string;
|
|
20
|
+
/** Canaux de l'authenticator (`internal`, `hybrid`, `usb`…). */
|
|
21
|
+
readonly transports: readonly string[];
|
|
22
|
+
/** Le credential PEUT être sauvegardé/synchronisé (BE flag). */
|
|
23
|
+
readonly backupEligible: boolean;
|
|
24
|
+
/** Le credential EST sauvegardé (BS flag) — sinon il meurt avec l'appareil. */
|
|
25
|
+
readonly backupState: boolean;
|
|
26
|
+
/** Une vérification utilisateur (biométrie/PIN) a déjà eu lieu. */
|
|
27
|
+
readonly uvInitialized: boolean;
|
|
28
|
+
/** Compteur de signatures (anti-clone WebAuthn §6.1.1). */
|
|
29
|
+
readonly signCount: number;
|
|
30
|
+
/** Nom donné à l'appareil par l'utilisateur, si renseigné. */
|
|
31
|
+
readonly nickname?: string;
|
|
32
|
+
/** Enrôlement (epoch ms). */
|
|
33
|
+
readonly createdAt: number;
|
|
34
|
+
/** Dernière authentification réussie (epoch ms), ou `null` = jamais servie. */
|
|
35
|
+
readonly lastUsedAt: number | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Requête de listing des passkeys — {@link IPageQuery} + les filtres qui ont un
|
|
39
|
+
* sens ici. `q` (hérité) = **préfixe** d'`userId`.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ À ne pas confondre avec {@link IWebAuthnCredentialStore.findByUser} : ce
|
|
42
|
+
* listing est le chemin FROID d'introspection admin ; `findByUser` est le chemin
|
|
43
|
+
* chaud du login, non paginé par nature.
|
|
44
|
+
*/
|
|
45
|
+
export interface IWebAuthnListQuery extends IPageQuery {
|
|
46
|
+
/** Restreindre à un porteur (sa liste d'appareils). */
|
|
47
|
+
userId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* `true` = passkeys sauvegardées/synchronisées seulement, `false` = celles
|
|
50
|
+
* liées à un seul appareil (les porteurs à risque de verrouillage), omis = les deux.
|
|
51
|
+
*/
|
|
52
|
+
backedUp?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** État mis à jour après une authentification réussie (WebAuthn §7.2). */
|
|
55
|
+
export interface WebAuthnAuthUpdate {
|
|
56
|
+
/** Nouveau compteur de signatures (anti-clone). */
|
|
57
|
+
signCount: number;
|
|
58
|
+
/** Nouvel état de sauvegarde (BS flag). */
|
|
59
|
+
backupState: boolean;
|
|
60
|
+
/** UV réalisée durant cette cérémonie. */
|
|
61
|
+
uvInitialized: boolean;
|
|
62
|
+
/** Horodatage de l'usage (epoch ms). */
|
|
63
|
+
lastUsedAt: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Store **pluggable** des credentials WebAuthn — découple le cœur du backend de
|
|
67
|
+
* persistance (mémoire / ORM / Redis). Convention-frère d'`ITokenStore` :
|
|
68
|
+
* le builtin `memory` est sans dépendance, les adapters lourds s'enregistrent
|
|
69
|
+
* depuis leur propre module (inversion de dépendance).
|
|
70
|
+
*/
|
|
71
|
+
export interface IWebAuthnCredentialStore {
|
|
72
|
+
/** Credential par son id (base64url), ou `null` — résolution à l'authentification. */
|
|
73
|
+
findById(credentialId: string): Promise<IWebAuthnCredential | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Tous les credentials d'un utilisateur — pour `allowCredentials`
|
|
76
|
+
* (authentification ciblée) et l'UX « mes appareils ».
|
|
77
|
+
*
|
|
78
|
+
* **Volontairement NON paginé** : `allowCredentials` doit être COMPLET ou il
|
|
79
|
+
* est faux — un authenticator dont la passkey manque de la liste ne peut pas
|
|
80
|
+
* répondre au défi, et le protocole WebAuthn n'offre aucun « page suivante »
|
|
81
|
+
* (le navigateur reçoit une liste unique et choisit). Ce qui borne cet appel
|
|
82
|
+
* est le plafond d'enrôlement (`passkeys.maxPerUser`), pas une pagination.
|
|
83
|
+
*/
|
|
84
|
+
findByUser(userId: string): Promise<IWebAuthnCredential[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Nombre de credentials d'un utilisateur — **natif** par backend (`COUNT`,
|
|
87
|
+
* `countDocuments`, `SCARD`), jamais un `findByUser().length`.
|
|
88
|
+
*
|
|
89
|
+
* Sert le plafond d'enrôlement (`passkeys.maxPerUser`) : le chemin
|
|
90
|
+
* d'enregistrement ne doit pas charger N credentials pour en compter le
|
|
91
|
+
* nombre, et le plafond est ce qui garantit que {@link findByUser} reste borné.
|
|
92
|
+
*/
|
|
93
|
+
countByUser(userId: string): Promise<number>;
|
|
94
|
+
/** Persiste un nouveau credential (fin de la cérémonie d'enregistrement). */
|
|
95
|
+
save(credential: IWebAuthnCredential): Promise<void>;
|
|
96
|
+
/** Met à jour l'état post-authentification (compteur, sauvegarde, UV, usage). */
|
|
97
|
+
update(credentialId: string, patch: WebAuthnAuthUpdate): Promise<void>;
|
|
98
|
+
/** Révoque un credential (l'utilisateur retire un appareil). */
|
|
99
|
+
delete(credentialId: string): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Page de passkeys pour le data plane admin — ne matérialise jamais plus d'une
|
|
102
|
+
* page, filtres appliqués au store, `publicKey` exclue (cf
|
|
103
|
+
* {@link IWebAuthnCredentialSummary}).
|
|
104
|
+
*
|
|
105
|
+
* Ordre contractuel (backends `offset`) : `createdAt` DESC, départagé par `id`
|
|
106
|
+
* ASC. Les backends **curseur** (Redis, dont l'index par utilisateur est un
|
|
107
|
+
* Set) n'ont pas d'ordre global : ils rendent des pages de taille variable et
|
|
108
|
+
* le client boucle sur `nextCursor` — capacité réduite déclarée, pas un défaut.
|
|
109
|
+
*/
|
|
110
|
+
listPage(query: IWebAuthnListQuery): Promise<IPage<IWebAuthnCredentialSummary>>;
|
|
111
|
+
/**
|
|
112
|
+
* Nombre de passkeys correspondant aux filtres (`COUNT` natif), ou **`-1`** si
|
|
113
|
+
* le backend ne sait pas compter à coût raisonnable (Redis : un total exigerait
|
|
114
|
+
* un `SCAN` complet O(N) sur un chemin froid — refusé).
|
|
115
|
+
*/
|
|
116
|
+
countCredentials(query: IWebAuthnListQuery): Promise<number>;
|
|
117
|
+
}
|
|
118
|
+
export default IWebAuthnCredentialStore;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint webhook sortant — une **destination** que Nodefony notifie quand un
|
|
3
|
+
* événement souscrit survient (modèle façon GitHub/Stripe).
|
|
4
|
+
*
|
|
5
|
+
* Le `secret` de signature est **chiffré au repos** (réversible, AES-256-GCM) :
|
|
6
|
+
* contrairement à une clé API (hachée, vérifiée seulement), le serveur doit le
|
|
7
|
+
* **relire** pour signer chaque livraison (HMAC). Le clair n'est jamais stocké,
|
|
8
|
+
* seulement {@link secretEnc} (blob opaque).
|
|
9
|
+
*/
|
|
10
|
+
export interface IWebhookEndpoint {
|
|
11
|
+
/** Identifiant public stable (`wh_<random>`). */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** URL de destination (validée anti-SSRF à l'enregistrement). */
|
|
14
|
+
readonly url: string;
|
|
15
|
+
/** Secret de signature **chiffré** au repos (blob `gcm1.…`). Jamais en clair. */
|
|
16
|
+
readonly secretEnc: string;
|
|
17
|
+
/**
|
|
18
|
+
* Actions d'audit souscrites (ex. `"login.success"`, `"user.created"`).
|
|
19
|
+
* `"*"` = toutes. La livraison ne part que si l'action de l'événement matche.
|
|
20
|
+
*/
|
|
21
|
+
readonly events: readonly string[];
|
|
22
|
+
/** Endpoint actif ? (désactivé = aucune livraison). */
|
|
23
|
+
readonly enabled: boolean;
|
|
24
|
+
/** Libellé humain optionnel (console admin). */
|
|
25
|
+
readonly description: string | null;
|
|
26
|
+
/** Slot multi-tenant (réservé P17) — `null` = global. */
|
|
27
|
+
readonly tenantId: string | null;
|
|
28
|
+
/** Identité de l'admin créateur (soft ref, traçabilité). */
|
|
29
|
+
readonly createdBy: string | null;
|
|
30
|
+
/** Création (epoch ms). */
|
|
31
|
+
readonly createdAt: number;
|
|
32
|
+
/** Dernière modification (epoch ms). */
|
|
33
|
+
readonly updatedAt: number;
|
|
34
|
+
/** Dernière tentative de livraison (epoch ms) ou `null`. */
|
|
35
|
+
readonly lastDeliveryAt: number | null;
|
|
36
|
+
/** Code HTTP de la dernière livraison, ou `null` (jamais livré / erreur réseau). */
|
|
37
|
+
readonly lastDeliveryStatus: number | null;
|
|
38
|
+
/** Message d'erreur de la dernière livraison, ou `null`. */
|
|
39
|
+
readonly lastDeliveryError: string | null;
|
|
40
|
+
/** Échecs consécutifs (auto-désactivation au-delà d'un seuil, façon GitHub). */
|
|
41
|
+
readonly failureCount: number;
|
|
42
|
+
/** Métadonnées extensibles (jamais de secret). */
|
|
43
|
+
readonly metadata: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Champs mutables d'un endpoint (PATCH). `id`/`createdAt`/`createdBy`/`tenantId`
|
|
47
|
+
* sont immuables après création.
|
|
48
|
+
*/
|
|
49
|
+
export type WebhookEndpointUpdate = Partial<Pick<IWebhookEndpoint, "url" | "secretEnc" | "events" | "enabled" | "description" | "updatedAt" | "lastDeliveryAt" | "lastDeliveryStatus" | "lastDeliveryError" | "failureCount" | "metadata">>;
|
|
50
|
+
/**
|
|
51
|
+
* Vue **publique** d'un endpoint (DTO console admin) — **sans** le secret
|
|
52
|
+
* chiffré. Le secret en clair n'est renvoyé qu'une fois, à la création/rotation.
|
|
53
|
+
*/
|
|
54
|
+
export type WebhookEndpointSummary = Omit<IWebhookEndpoint, "secretEnc">;
|
|
55
|
+
/**
|
|
56
|
+
* Trace d'une livraison (historique « récentes », façon GitHub/Stripe) — ce que
|
|
57
|
+
* Nodefony a **envoyé** à l'endpoint + la **réponse** observée. Stocké en RAM,
|
|
58
|
+
* borné, **par pod** (observabilité éphémère, non persistée). Aucun secret (le
|
|
59
|
+
* `webhook-signature` n'en révèle rien ; le corps signé est l'événement public).
|
|
60
|
+
*/
|
|
61
|
+
export interface IWebhookDelivery {
|
|
62
|
+
/** Horodatage de la tentative (epoch ms). */
|
|
63
|
+
readonly ts: number;
|
|
64
|
+
/** `webhook-id` du message livré. */
|
|
65
|
+
readonly messageId: string;
|
|
66
|
+
/** Type d'événement (= action d'audit, ex. `login.failure`). */
|
|
67
|
+
readonly type: string;
|
|
68
|
+
/** Numéro de tentative (0 = 1ʳᵉ ; > 0 = retry). */
|
|
69
|
+
readonly attempt: number;
|
|
70
|
+
/** Livraison acceptée (2xx) ? */
|
|
71
|
+
readonly ok: boolean;
|
|
72
|
+
/** Code HTTP, ou `null` (réseau/timeout/SSRF). */
|
|
73
|
+
readonly status: number | null;
|
|
74
|
+
/** Message d'erreur, ou `null` si OK. */
|
|
75
|
+
readonly error: string | null;
|
|
76
|
+
/** Durée de la tentative (ms). */
|
|
77
|
+
readonly durationMs: number;
|
|
78
|
+
/** Corps JSON envoyé (enveloppe `{id,timestamp,type,data}`), tronqué. */
|
|
79
|
+
readonly requestBody: string;
|
|
80
|
+
/** Début du corps de réponse du destinataire (tronqué), ou `null`. */
|
|
81
|
+
readonly responseBody: string | null;
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { IPage, IPageQuery, ISortableSource } from "nodefony";
|
|
2
|
+
import type { IWebhookEndpoint, WebhookEndpointUpdate } from "./IWebhookEndpoint.js";
|
|
3
|
+
/**
|
|
4
|
+
* Requête de **listing paginé** d'endpoints webhook (data plane admin) — le
|
|
5
|
+
* contrat de page standard du core ({@link IPageQuery}) enrichi des filtres
|
|
6
|
+
* propres aux endpoints.
|
|
7
|
+
*
|
|
8
|
+
* `q` (hérité) = sous-chaîne **insensible à la casse** cherchée dans `url` **ou**
|
|
9
|
+
* `description` — la question posée par un humain devant la console (« où part
|
|
10
|
+
* mon webhook stripe ? ») porte sur ces deux champs, jamais sur l'id.
|
|
11
|
+
*/
|
|
12
|
+
export interface IWebhookListQuery extends IPageQuery {
|
|
13
|
+
/** `true` = actifs seulement, `false` = désactivés seulement, omis = les deux. */
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Ne garder que les endpoints **abonnés à cet événement** (appartenance au
|
|
17
|
+
* tableau `events`). Répond à la question d'exploitation « qui écoute
|
|
18
|
+
* `user.created` ? ». Non portable au `Criteria` générique (containment dans un
|
|
19
|
+
* tableau JSON) → chaque backend l'implémente nativement.
|
|
20
|
+
*/
|
|
21
|
+
event?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `true` = seulement les endpoints **en échec** (au moins un échec consécutif
|
|
24
|
+
* courant, `failureCount > 0`), `false` = seulement ceux qui vont bien, omis =
|
|
25
|
+
* les deux.
|
|
26
|
+
*
|
|
27
|
+
* Au contrat plutôt que déduit d'un `order` : c'est la question d'exploitation
|
|
28
|
+
* la plus fréquente — « qu'est-ce qui casse ? » — et la carte qui l'affiche
|
|
29
|
+
* doit pouvoir être cliquée pour filtrer le tableau sur la même population.
|
|
30
|
+
* Portable partout (comparaison sur une colonne entière indexable).
|
|
31
|
+
*/
|
|
32
|
+
failing?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Persistance des **endpoints webhook** (configuration durable, pas un cache).
|
|
36
|
+
* Backend interchangeable (Memory dev/test · Drizzle SQL · Mongoose) via le
|
|
37
|
+
* registre {@link ../src/webhook/webhookStoreRegistry}. **Redis n'est PAS un
|
|
38
|
+
* store d'endpoints** (config durable ≠ éphémère) — il servira la *queue de
|
|
39
|
+
* livraison* cross-pod (slice cluster), pas ce contrat.
|
|
40
|
+
*
|
|
41
|
+
* Volume attendu : faible (dizaines d'endpoints), lecture fréquente par le
|
|
42
|
+
* dispatcher (qui en garde un snapshot mémoire), écriture rare (CRUD admin).
|
|
43
|
+
*/
|
|
44
|
+
export interface IWebhookStore extends ISortableSource {
|
|
45
|
+
/** Insère un nouvel endpoint. */
|
|
46
|
+
save(endpoint: IWebhookEndpoint): Promise<void>;
|
|
47
|
+
/** Charge un endpoint par id, ou `null`. */
|
|
48
|
+
findById(id: string): Promise<IWebhookEndpoint | null>;
|
|
49
|
+
/** Applique un patch partiel (champs mutables) ; no-op si id absent. */
|
|
50
|
+
update(id: string, patch: WebhookEndpointUpdate): Promise<void>;
|
|
51
|
+
/** Supprime un endpoint ; no-op si id absent. */
|
|
52
|
+
delete(id: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* **Tous** les endpoints — réservé au **snapshot du dispatcher** (le service
|
|
55
|
+
* garde en mémoire la table complète des abonnements pour router un événement
|
|
56
|
+
* sans I/O, et la recharge au boot puis après chaque écriture CRUD).
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ Énumération complète assumée **par conception** ici : le dispatcher doit
|
|
59
|
+
* connaître TOUS les abonnements pour ne pas rater une livraison. C'est un
|
|
60
|
+
* cold-path (boot + CRUD admin), jamais une requête d'affichage. Pour lister
|
|
61
|
+
* dans une console, utiliser {@link IWebhookStore.listPage}.
|
|
62
|
+
*/
|
|
63
|
+
listAll(): Promise<IWebhookEndpoint[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Liste **paginée** d'endpoints pour le data plane admin — ne matérialise
|
|
66
|
+
* jamais plus d'une page, filtres {@link IWebhookListQuery} appliqués **au
|
|
67
|
+
* store** (jamais après un chargement complet).
|
|
68
|
+
*
|
|
69
|
+
* Ordre par défaut : `createdAt` DESC (le plus récent d'abord), départagé par
|
|
70
|
+
* `id` ASC — sans ce tiebreaker deux endpoints créés dans la même milliseconde
|
|
71
|
+
* pourraient changer de page entre deux appels et l'un d'eux ne jamais
|
|
72
|
+
* apparaître. Un `order` explicite le remplace, dans la limite de
|
|
73
|
+
* {@link IWebhookStore.sortableFields} ; il s'applique **avant** le découpage
|
|
74
|
+
* en pages, jamais sur la tranche déjà extraite.
|
|
75
|
+
*/
|
|
76
|
+
listPage(query: IWebhookListQuery): Promise<IPage<IWebhookEndpoint>>;
|
|
77
|
+
/**
|
|
78
|
+
* Nombre d'endpoints correspondant aux filtres (`COUNT` natif) — base du
|
|
79
|
+
* `total` d'une page et des compteurs de la console.
|
|
80
|
+
*
|
|
81
|
+
* @returns le compte exact ; `-1` si le backend ne sait pas compter à coût
|
|
82
|
+
* raisonnable (« je ne sais pas » explicite, jamais un total inventé).
|
|
83
|
+
*/
|
|
84
|
+
countEndpoints(query: IWebhookListQuery): Promise<number>;
|
|
85
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { IToken } from "./IToken.js";
|
|
2
|
+
export type { IAuthenticator } from "./IAuthenticator.js";
|
|
3
|
+
export type { ISecuredArea } from "./ISecuredArea.js";
|
|
4
|
+
export type { IFirewall } from "./IFirewall.js";
|
|
5
|
+
export type { IAccessVoter } from "./IAccessVoter.js";
|
|
6
|
+
export type { IAuthorizationService } from "./IAuthorizationService.js";
|
|
7
|
+
export { VoterVote } from "./IAccessVoter.js";
|
|
8
|
+
export type { IAccessTokenRecord, ITokenStore, ITokenListQuery, ITokenUsage, IResourcePermission, TokenRevokeReason, TokenStatus, } from "./ITokenStore.js";
|
|
9
|
+
export type { IJwtKeystore, IJwtSigningKey } from "./IJwtKeystore.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Accès refusé — `code = 403`. Levée par l'autorisation (un `@IsGranted` non
|
|
4
|
+
* satisfait, un voter DENY) : l'utilisateur EST authentifié mais n'a pas le droit.
|
|
5
|
+
* À distinguer d'{@link AuthenticationError} (401 = pas authentifié).
|
|
6
|
+
*/
|
|
7
|
+
export declare class AccessDeniedError extends nodefonyError {
|
|
8
|
+
constructor(message?: string | Error);
|
|
9
|
+
}
|
|
10
|
+
export default AccessDeniedError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Erreur de **gestion** d'une clé API (création/révocation) — porte un `code`
|
|
4
|
+
* HTTP que l'adaptateur framework mappe par duck-typing (il n'importe jamais les
|
|
5
|
+
* classes de `@nodefony/security`).
|
|
6
|
+
*
|
|
7
|
+
* - `400` — entrée invalide (nom vide, scope hors catalogue) ;
|
|
8
|
+
* - `409` — plafond `apiKeys.maxPerSubject` atteint ;
|
|
9
|
+
* - `503` — store de jetons indisponible (clés activées mais non provisionnées).
|
|
10
|
+
*
|
|
11
|
+
* Distincte de l'**authentification** d'une clé présentée (→ `AuthenticationError`
|
|
12
|
+
* 401, message uniforme anti-énumération).
|
|
13
|
+
*/
|
|
14
|
+
export declare class ApiKeyError extends nodefonyError {
|
|
15
|
+
constructor(message: string, code: 400 | 409 | 503);
|
|
16
|
+
}
|
|
17
|
+
export default ApiKeyError;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Échec d'authentification — `code = 401`. Levée par un {@link IAuthenticator}
|
|
4
|
+
* quand le credential est absent/invalide, ou par le firewall en Zero Trust
|
|
5
|
+
* (zone protégée + visiteur anonyme + route sans `@Anonymous`).
|
|
6
|
+
*/
|
|
7
|
+
export declare class AuthenticationError extends nodefonyError {
|
|
8
|
+
constructor(message?: string | Error);
|
|
9
|
+
}
|
|
10
|
+
export default AuthenticationError;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Mutation cross-site bloquée — `code = 403` (RFC 9110 §15.5.4 : le serveur a
|
|
4
|
+
* compris la requête mais refuse de l'honorer).
|
|
5
|
+
*
|
|
6
|
+
* Levée par {@link Csrf} sur une méthode state-changing (POST/PUT/PATCH/DELETE)
|
|
7
|
+
* dont la provenance est tierce : `Sec-Fetch-Site: cross-site` (défense primaire
|
|
8
|
+
* Fetch Metadata, W3C — infalsifiable par un script attaquant) ou, à défaut de
|
|
9
|
+
* Fetch Metadata, un `Origin`/`Referer` étranger aux origines de l'app (fallback).
|
|
10
|
+
*
|
|
11
|
+
* Le message reste GÉNÉRIQUE : la politique CSRF (en-têtes inspectés, whitelist)
|
|
12
|
+
* ne fuite jamais au client. Distincte du 401 (qui es-tu ?) et de l'AccessDenied
|
|
13
|
+
* (autorisé mais rôle insuffisant) : ici l'identité importe peu, c'est la
|
|
14
|
+
* PROVENANCE de la requête qui est rejetée.
|
|
15
|
+
*/
|
|
16
|
+
export declare class CsrfError extends nodefonyError {
|
|
17
|
+
constructor(message?: string | Error);
|
|
18
|
+
}
|
|
19
|
+
export default CsrfError;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* La ressource demandée à l'émission ne peut pas être servie — `code = 400`,
|
|
4
|
+
* code d'erreur OAuth `invalid_target` (RFC 8707 §2).
|
|
5
|
+
*
|
|
6
|
+
* Le paramètre `resource` dit POUR QUI le jeton est demandé. Trois raisons de
|
|
7
|
+
* refuser, et la RFC les couvre d'un seul code : « The requested resource is
|
|
8
|
+
* invalid, missing, unknown, or malformed. »
|
|
9
|
+
*
|
|
10
|
+
* **Pourquoi refuser plutôt qu'ignorer.** Un `resource` accepté puis jeté rend
|
|
11
|
+
* un jeton parfaitement valide… pour quelqu'un d'autre. Le client croit tenir
|
|
12
|
+
* une clé pour la porte A, la présente, reçoit un `401`, et n'a aucun moyen de
|
|
13
|
+
* comprendre que sa demande n'a jamais été honorée : l'erreur se manifeste chez
|
|
14
|
+
* la ressource, loin de l'endroit où elle a été commise. Refuser à l'émission
|
|
15
|
+
* met le diagnostic là où la faute est.
|
|
16
|
+
*
|
|
17
|
+
* **Le message est constant** et ne nomme pas les audiences acceptées : les
|
|
18
|
+
* énumérer offrirait la carte des ressources protégées de l'application à qui
|
|
19
|
+
* possède un simple identifiant. La valeur refusée, elle, vient du client — la
|
|
20
|
+
* lui rendre ne lui apprend rien.
|
|
21
|
+
*/
|
|
22
|
+
export declare class InvalidTargetError extends nodefonyError {
|
|
23
|
+
/** Code d'erreur OAuth à rendre au client (RFC 6749 §5.2 / RFC 8707 §2). */
|
|
24
|
+
readonly oauthError = "invalid_target";
|
|
25
|
+
/** Ce que le client a demandé, tel qu'il l'a écrit — pour le journal. */
|
|
26
|
+
readonly requested?: string;
|
|
27
|
+
/**
|
|
28
|
+
* @param description - raison, destinée à `error_description` (aucune fuite :
|
|
29
|
+
* elle qualifie la DEMANDE, jamais la configuration du serveur)
|
|
30
|
+
* @param requested - la valeur refusée, pour le journal
|
|
31
|
+
*/
|
|
32
|
+
constructor(description: string, requested?: string);
|
|
33
|
+
}
|
|
34
|
+
export default InvalidTargetError;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* URL rejetée par la protection SSRF — `code = 422`. Levée quand une URL sortante
|
|
4
|
+
* (endpoint webhook, fetch applicatif…) est syntaxiquement valide mais cible une
|
|
5
|
+
* ressource **interdite** : protocole non autorisé, identifiants embarqués, hôte
|
|
6
|
+
* non résolvable, ou IP non publique (loopback, privée, link-local, métadonnées
|
|
7
|
+
* cloud `169.254.169.254`…). Sémantique alignée sur GitHub (422 à l'enregistrement
|
|
8
|
+
* d'un webhook invalide).
|
|
9
|
+
*/
|
|
10
|
+
export declare class SsrfError extends nodefonyError {
|
|
11
|
+
constructor(message?: string | Error);
|
|
12
|
+
}
|
|
13
|
+
export default SsrfError;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Tentatives de login trop rapprochées — `code = 429` (RFC 6585 §4).
|
|
4
|
+
*
|
|
5
|
+
* Levée par `UserPasswordAuthenticator` quand le backoff progressif
|
|
6
|
+
* ({@link LoginThrottler}) bloque encore l'identifiant saisi. Distincte du 401 :
|
|
7
|
+
* un client légitime doit savoir QU'ATTENDRE (header `Retry-After`, posé par le
|
|
8
|
+
* firewall), pas re-soumettre en boucle. Le message reste générique — la
|
|
9
|
+
* politique de throttle (seuils, compteurs) n'est jamais détaillée au client.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ThrottledError extends nodefonyError {
|
|
12
|
+
/** Secondes restantes avant la prochaine tentative autorisée (header `Retry-After`). */
|
|
13
|
+
readonly retryAfterS: number;
|
|
14
|
+
constructor(retryAfterS: number);
|
|
15
|
+
}
|
|
16
|
+
export default ThrottledError;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Le jeton n'a pas pu être VÉRIFIÉ — `code = 503`, et surtout **pas 401**.
|
|
4
|
+
*
|
|
5
|
+
* Levée quand ce qui sait valider un jeton est absent ou en panne : aucun
|
|
6
|
+
* vérificateur posé au conteneur, émetteur injoignable, jeu de clés
|
|
7
|
+
* inutilisable. Le jeton n'est alors ni valide ni invalide — on n'en sait
|
|
8
|
+
* rien, et c'est une information différente.
|
|
9
|
+
*
|
|
10
|
+
* **Pourquoi une erreur distincte.** Répondre 401 à une panne envoie le client
|
|
11
|
+
* chercher un autre jeton, qui échouera pareil : la boucle de renouvellement
|
|
12
|
+
* remplace la panne par une tempête de requêtes, pendant que le tableau de bord
|
|
13
|
+
* affiche une hausse d'« échecs d'authentification » qui ne désigne aucun
|
|
14
|
+
* coupable. Le 503 dit la vérité — le service ne peut pas répondre — et le
|
|
15
|
+
* client légitime attend au lieu d'insister.
|
|
16
|
+
*
|
|
17
|
+
* C'est la même distinction que celle tenue par le vérificateur lui-même, où
|
|
18
|
+
* un refus rend `null` et une panne lève : la porte doit la conserver jusqu'à
|
|
19
|
+
* la réponse, sinon elle est perdue là où elle sert.
|
|
20
|
+
*
|
|
21
|
+
* Le message est **constant**, et c'est structurel : il est rendu au client. La
|
|
22
|
+
* cause technique — nom de l'émetteur défaillant, URL du jeu de clés, erreur
|
|
23
|
+
* réseau — vit dans {@link detail}, que seul le journal lit. Composer la cause
|
|
24
|
+
* dans le message revient à publier la topologie interne de l'authentification
|
|
25
|
+
* à qui présente un jeton quelconque, et le rendu d'erreur de développement y
|
|
26
|
+
* ajoute la pile d'appels par-dessus.
|
|
27
|
+
*/
|
|
28
|
+
export declare class UnverifiableTokenError extends nodefonyError {
|
|
29
|
+
/** Cause technique, destinée au JOURNAL — jamais au client. */
|
|
30
|
+
readonly detail?: string;
|
|
31
|
+
/**
|
|
32
|
+
* @param detail - cause technique pour le journal ; n'apparaît jamais dans le
|
|
33
|
+
* message rendu au client
|
|
34
|
+
*/
|
|
35
|
+
constructor(detail?: string);
|
|
36
|
+
}
|
|
37
|
+
export default UnverifiableTokenError;
|