@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,43 @@
|
|
|
1
|
+
import type { JSONWebKeySet } from "jose";
|
|
2
|
+
import type { IJwtKeystore, IJwtSigningKey } from "../../contracts/IJwtKeystore.js";
|
|
3
|
+
/** Journalisation injectée (le keystore n'est pas un Service — il reçoit un log). */
|
|
4
|
+
type LogFn = (message: string, severity: string) => void;
|
|
5
|
+
/** Source de clé configurée (`config.jwt.keystore`). */
|
|
6
|
+
interface KeystoreSource {
|
|
7
|
+
/** JWK Set (clés privées) injecté depuis l'env — source `env` (prod). */
|
|
8
|
+
readonly keySetJson?: string;
|
|
9
|
+
/** Dossier de persistance `keyset.json` — source `fichier` (opt-in dev/VPS). */
|
|
10
|
+
readonly dir?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Keystore Ed25519 — implémentation de référence d'{@link IJwtKeystore}.
|
|
14
|
+
*
|
|
15
|
+
* Résout la clé de signature selon une **priorité** (jamais d'auto-génération en
|
|
16
|
+
* clair « par défaut » en prod) :
|
|
17
|
+
* 1. **env** — `config.jwt.keystore.keySetJson` (JWK Set injecté par l'app depuis
|
|
18
|
+
* son catalogue d'env) : prod cloud, secret géré hors-app, même clé sur tous
|
|
19
|
+
* les pods.
|
|
20
|
+
* 2. **fichier** — `config.jwt.keystore.dir/keyset.json` (écrit en mode 600,
|
|
21
|
+
* généré si absent) : opt-in dev/VPS mono-machine. Le mode effectif est
|
|
22
|
+
* **constaté** après coup : un système de fichiers qui n'applique pas les
|
|
23
|
+
* permissions POSIX (NTFS, FAT, NFS sans mapping) déclenche un **warning**
|
|
24
|
+
* plutôt qu'une garantie silencieusement fausse.
|
|
25
|
+
* 3. **mémoire** — aucune source → clé éphémère générée au 1ᵉʳ usage + **warning**
|
|
26
|
+
* (perdue au redémarrage = refresh invalidés, incohérente en cluster).
|
|
27
|
+
*
|
|
28
|
+
* jose est importé **paresseusement** (dep lourde — règle perf P6) au premier
|
|
29
|
+
* usage ; le boot ne paie rien si le JWT n'est jamais sollicité. Le chargement
|
|
30
|
+
* est mémoïsé (une seule résolution concurrente).
|
|
31
|
+
*
|
|
32
|
+
* @remarks Race au 1ᵉʳ boot d'un **cluster** sans clé pré-provisionnée : deux
|
|
33
|
+
* workers peuvent générer puis écrire des clés différentes (le dernier `rename`
|
|
34
|
+
* gagne). En prod, provisionner la clé hors-bande (`keySetJson`/SecretProvider
|
|
35
|
+
* P16) élimine ce cas — c'est précisément la source recommandée.
|
|
36
|
+
*/
|
|
37
|
+
export declare class JwtKeystore implements IJwtKeystore {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(source: KeystoreSource, log: LogFn);
|
|
40
|
+
getSigningKey(): Promise<IJwtSigningKey>;
|
|
41
|
+
getPublicJWKS(): Promise<JSONWebKeySet>;
|
|
42
|
+
}
|
|
43
|
+
export default JwtKeystore;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type { IAccessTokenRecord, ITokenListQuery, ITokenStore, ITokenUsage, TokenRevokeReason } from "../../contracts/ITokenStore.js";
|
|
3
|
+
/**
|
|
4
|
+
* Filtre un record contre une requête de listing — prédicat de RÉFÉRENCE du
|
|
5
|
+
* contrat, réutilisé par les implémentations qui évaluent en mémoire.
|
|
6
|
+
*
|
|
7
|
+
* @param now - instant de référence pour l'état du jeton (`status`). Requis :
|
|
8
|
+
* « expiré » n'a pas de sens sans une horloge, et la lire ici ferait dépendre
|
|
9
|
+
* le résultat du moment du test plutôt que de la donnée.
|
|
10
|
+
*/
|
|
11
|
+
export declare function matchesTokenQuery(record: IAccessTokenRecord, query: ITokenListQuery, now: number): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Instantané sérialisable de l'état d'un store en mémoire — base de la
|
|
14
|
+
* persistance fichier ({@link MemoryTokenStore.snapshot}/`restore`) et de
|
|
15
|
+
* l'inspection. Les index dérivés (par hash/famille/sujet) ne sont PAS
|
|
16
|
+
* sérialisés : ils sont reconstruits depuis `records` au `restore`.
|
|
17
|
+
*/
|
|
18
|
+
export interface TokenStoreSnapshot {
|
|
19
|
+
records: IAccessTokenRecord[];
|
|
20
|
+
deniedJti: Array<[string, number]>;
|
|
21
|
+
invalidBefore: Array<[string, number]>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Store de jetons **en mémoire** — implémentation de référence d'{@link ITokenStore}.
|
|
25
|
+
*
|
|
26
|
+
* 0 dépendance, idéale pour le développement mono-process et les **tests**. NON
|
|
27
|
+
* partagée entre process (pas de cluster) et **volatile** (tout est perdu au
|
|
28
|
+
* redémarrage) → en production multi-process, utiliser un adapter ORM ou Redis.
|
|
29
|
+
*
|
|
30
|
+
* Perf/mémoire : les `Map` n'existent que si le store est instancié (JWT activé),
|
|
31
|
+
* jamais sur le hot path par requête. La denylist `jti` est bornée par un
|
|
32
|
+
* **balayage amorti** (purge des entrées expirées tous les 256 ajouts) doublé
|
|
33
|
+
* d'une expiration paresseuse à la lecture — pas de minuterie, pas de fuite.
|
|
34
|
+
*
|
|
35
|
+
* Horloge injectable (`now`) pour des tests déterministes (pattern `LoginThrottler`).
|
|
36
|
+
*/
|
|
37
|
+
export declare class MemoryTokenStore implements ITokenStore {
|
|
38
|
+
#private;
|
|
39
|
+
/**
|
|
40
|
+
* {@inheritDoc ITokenStore.sortableFields}
|
|
41
|
+
*
|
|
42
|
+
* Le store mémoire porte l'enregistrement complet : il sait donc trier tout le
|
|
43
|
+
* vocabulaire public, sans réduction de capacité.
|
|
44
|
+
*/
|
|
45
|
+
readonly sortableFields: readonly ["createdAt", "name", "subjectId", "id"];
|
|
46
|
+
constructor(now?: () => number, retentionRevokedMs?: number);
|
|
47
|
+
put(record: IAccessTokenRecord): Promise<void>;
|
|
48
|
+
findById(id: string): Promise<IAccessTokenRecord | null>;
|
|
49
|
+
findByHash(secretHash: string): Promise<IAccessTokenRecord | null>;
|
|
50
|
+
findBySubject(subjectId: string): Promise<IAccessTokenRecord[]>;
|
|
51
|
+
listAll(): Promise<IAccessTokenRecord[]>;
|
|
52
|
+
listPage(query: ITokenListQuery): Promise<IPage<IAccessTokenRecord>>;
|
|
53
|
+
countTokens(query: ITokenListQuery): Promise<number>;
|
|
54
|
+
markUsed(id: string, usage: ITokenUsage): Promise<void>;
|
|
55
|
+
revoke(id: string, reason: TokenRevokeReason): Promise<void>;
|
|
56
|
+
revokeFamily(family: string, reason: TokenRevokeReason): Promise<void>;
|
|
57
|
+
denyJti(jti: string, expiresAt: number): Promise<void>;
|
|
58
|
+
isJtiDenied(jti: string): Promise<boolean>;
|
|
59
|
+
revokeAllForSubject(subjectId: string, invalidBefore: number): Promise<void>;
|
|
60
|
+
getInvalidBefore(subjectId: string): Promise<number | null>;
|
|
61
|
+
gc(now?: number): Promise<number>;
|
|
62
|
+
/** Instantané sérialisable de l'état courant (records + denylist + seuils). */
|
|
63
|
+
snapshot(): TokenStoreSnapshot;
|
|
64
|
+
/** Remplace l'état par celui d'un instantané (reconstruit les index dérivés). */
|
|
65
|
+
restore(snapshot: TokenStoreSnapshot): void;
|
|
66
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type * as Jose from "jose";
|
|
2
|
+
import { type IAccessPrincipal } from "nodefony";
|
|
3
|
+
/**
|
|
4
|
+
* Un émetteur en qui l'application accepte de faire confiance.
|
|
5
|
+
*
|
|
6
|
+
* Le fait qu'il n'y ait pas de valeur par défaut pour `issuer` est le cœur du
|
|
7
|
+
* dispositif : **la liste des émetteurs est fermée et vient de la
|
|
8
|
+
* configuration**, jamais d'un jeton. Le `iss` présenté ne sert qu'à choisir
|
|
9
|
+
* DANS cette liste — il ne peut donc pas désigner un serveur que l'application
|
|
10
|
+
* n'a pas nommé, et aucune requête sortante ne peut être provoquée par un
|
|
11
|
+
* appelant anonyme vers une URL de son choix.
|
|
12
|
+
*/
|
|
13
|
+
export interface ITrustedIssuer {
|
|
14
|
+
/** Identifiant canonique de l'émetteur (`iss` attendu dans les jetons). */
|
|
15
|
+
issuer: string;
|
|
16
|
+
/**
|
|
17
|
+
* Jeu de clés, quand on ne veut pas de découverte.
|
|
18
|
+
*
|
|
19
|
+
* Utile pour un émetteur qui ne publie pas de métadonnées, et pour supprimer
|
|
20
|
+
* une requête au démarrage à froid. Déclaré, il fait autorité : rien n'est
|
|
21
|
+
* découvert.
|
|
22
|
+
*/
|
|
23
|
+
jwksUri?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Jeu de clés fourni LOCALEMENT — aucune requête, aucune découverte.
|
|
26
|
+
*
|
|
27
|
+
* ⭐ **Le cas qui l'exige : l'émetteur, c'est CETTE application.** Elle signe
|
|
28
|
+
* ses propres jetons, elle a donc déjà les clés publiques en mémoire. Aller
|
|
29
|
+
* les relire par HTTP chez elle-même ajoute à une opération purement locale
|
|
30
|
+
* une dépendance au réseau, au DNS et à TLS — et c'est exactement là que ça
|
|
31
|
+
* casse : en développement, l'application se sert un certificat que le
|
|
32
|
+
* magasin d'autorités de Node ne connaît pas, si bien que la vérification
|
|
33
|
+
* échouait en `SELF_SIGNED_CERT_IN_CHAIN` alors que le jeton était parfait,
|
|
34
|
+
* que la clé était à portée de main, et que `curl` joignait la même URL sans
|
|
35
|
+
* broncher. En production, le même aller-retour ferait dépendre
|
|
36
|
+
* l'authentification de l'entrée réseau du pod.
|
|
37
|
+
*
|
|
38
|
+
* Appelé à CHAQUE résolution, jamais mémoïsé : une rotation de clés locale
|
|
39
|
+
* est alors prise en compte sans redémarrage, et le coût — analyser un jeu de
|
|
40
|
+
* une ou deux clés — est sans commune mesure avec la vérification de
|
|
41
|
+
* signature qui suit.
|
|
42
|
+
*/
|
|
43
|
+
localJwks?: () => Promise<Jose.JSONWebKeySet>;
|
|
44
|
+
/**
|
|
45
|
+
* Algorithmes de signature acceptés — **allowlist côté serveur**.
|
|
46
|
+
*
|
|
47
|
+
* RFC 8725 §3.1 : l'algorithme ne se déduit JAMAIS de l'en-tête du jeton.
|
|
48
|
+
* Tous asymétriques, et c'est structurel : les clés viennent d'un jeu PUBLIC,
|
|
49
|
+
* donc accepter un algorithme à secret partagé (`HS*`) laisserait un attaquant
|
|
50
|
+
* signer avec la clé publique de l'émetteur, que tout le monde peut lire.
|
|
51
|
+
*/
|
|
52
|
+
algorithms: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Valeur exigée de l'en-tête `typ` (RFC 9068 : `at+jwt`), ou rien.
|
|
55
|
+
*
|
|
56
|
+
* Non exigé par défaut : le parc réel est très inégal sur ce point, et un
|
|
57
|
+
* défaut strict serait désactivé en bloc à la première intégration plutôt que
|
|
58
|
+
* réglé finement. La séparation entre jetons est déjà assurée par l'audience,
|
|
59
|
+
* qui, elle, n'est pas facultative.
|
|
60
|
+
*/
|
|
61
|
+
typ?: string;
|
|
62
|
+
/** Claims dont la PRÉSENCE est exigée, en plus de `iss`/`aud`/`sub`. */
|
|
63
|
+
requiredClaims?: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
/** Réglages du vérificateur — au-delà de la liste des émetteurs. */
|
|
66
|
+
export interface IRemoteJwtVerifierOptions {
|
|
67
|
+
/** Les émetteurs de confiance. Vide = le vérificateur ne sert à rien. */
|
|
68
|
+
issuers: readonly ITrustedIssuer[];
|
|
69
|
+
/** Délai maximal d'une requête vers un émetteur (ms). */
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
/** Fenêtre pendant laquelle on ne redemande PAS le jeu de clés (ms). */
|
|
72
|
+
cooldownMs?: number;
|
|
73
|
+
/** Âge maximal du jeu de clés en cache avant rafraîchissement (ms). */
|
|
74
|
+
cacheMaxAgeMs?: number;
|
|
75
|
+
/** Tolérance d'horloge sur `exp`/`nbf` (secondes). */
|
|
76
|
+
clockToleranceS?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Implémentation de `fetch` — le seul moyen d'éprouver ce code SANS réseau.
|
|
79
|
+
*
|
|
80
|
+
* C'est ce qui permet aux tests de jouer une rotation de clés, un émetteur
|
|
81
|
+
* qui ment sur son identité ou un délai dépassé, de façon déterministe et
|
|
82
|
+
* sans démarrer quoi que ce soit.
|
|
83
|
+
*/
|
|
84
|
+
fetch?: typeof globalThis.fetch;
|
|
85
|
+
/** Journal d'audit — reçoit la cause FINE, que le client ne voit jamais. */
|
|
86
|
+
log?: (message: string) => void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Vérificateur de jetons d'accès émis par un **serveur d'autorisation tiers**.
|
|
90
|
+
*
|
|
91
|
+
* C'est la pièce qui manquait pour que le rôle *serveur de ressource* du cœur
|
|
92
|
+
* (`nodefony/src/oauth/`) soit autre chose qu'un refus poli : il sait publier ce
|
|
93
|
+
* qu'il protège et dire où prendre un jeton, mais rien, jusqu'ici, ne savait
|
|
94
|
+
* LIRE ce jeton. `JwtAuthenticator` ne vérifie que les jetons émis par
|
|
95
|
+
* Nodefony lui-même (jeu de clés local) ; ici, les clés appartiennent à
|
|
96
|
+
* quelqu'un d'autre, arrivent par le réseau et tournent sans prévenir.
|
|
97
|
+
*
|
|
98
|
+
* ## Ce qui vaut garantie
|
|
99
|
+
*
|
|
100
|
+
* - **L'audience est obligatoire et vient de l'APPELANT** — jamais du jeton. Un
|
|
101
|
+
* jeton parfaitement valide, émis par un émetteur de confiance, pour un AUTRE
|
|
102
|
+
* service, est refusé (RFC 8707 §2). C'est la seule chose qui empêche le
|
|
103
|
+
* rejeu d'un jeton légitime d'une ressource vers une autre.
|
|
104
|
+
* - **L'algorithme est imposé par la configuration** (RFC 8725 §3.1), jamais lu
|
|
105
|
+
* dans l'en-tête ; `alg: none` n'existe pas pour cette API.
|
|
106
|
+
* - **Les clés viennent du `jwks_uri` de l'émetteur**, jamais d'un `jku` ou
|
|
107
|
+
* d'un `jwk` porté par le jeton (§3.5) — sans quoi un attaquant fournirait
|
|
108
|
+
* la clé qui valide sa propre signature.
|
|
109
|
+
* - **La liste des émetteurs est fermée** : un `iss` inconnu est refusé avant
|
|
110
|
+
* toute requête sortante.
|
|
111
|
+
*
|
|
112
|
+
* ## Ce que cette classe ne fait pas
|
|
113
|
+
*
|
|
114
|
+
* Elle n'établit pas d'utilisateur applicatif : elle rend un sujet et des
|
|
115
|
+
* scopes. Rattacher ce sujet à un compte local (approvisionnement à la volée,
|
|
116
|
+
* comptes de service) est une décision d'application, pas de protocole — et
|
|
117
|
+
* l'entremêler ici rendrait impossible d'accepter un appelant purement machine,
|
|
118
|
+
* qui est précisément le cas d'usage.
|
|
119
|
+
*
|
|
120
|
+
* @see references/rfc/ietf/rfc8707.txt — l'audience, qui LIE un jeton à CE service
|
|
121
|
+
*/
|
|
122
|
+
export declare class RemoteJwtVerifier {
|
|
123
|
+
#private;
|
|
124
|
+
/**
|
|
125
|
+
* @param options - émetteurs de confiance et réglages réseau
|
|
126
|
+
* @throws Error si un émetteur est invalide, dupliqué, ou déclare un
|
|
127
|
+
* algorithme à secret partagé
|
|
128
|
+
*/
|
|
129
|
+
constructor(options: IRemoteJwtVerifierOptions);
|
|
130
|
+
/** Nombre d'émetteurs de confiance — pour l'introspection et les journaux. */
|
|
131
|
+
get size(): number;
|
|
132
|
+
/**
|
|
133
|
+
* Vérifie un jeton porté, pour UNE ressource donnée.
|
|
134
|
+
*
|
|
135
|
+
* Conforme au contrat `IAccessTokenVerifier` du cœur : un refus est un `null`,
|
|
136
|
+
* jamais une exception. Les exceptions sont réservées aux pannes — un
|
|
137
|
+
* émetteur injoignable n'est pas un jeton invalide.
|
|
138
|
+
*
|
|
139
|
+
* @param token - le jeton brut, tel que présenté
|
|
140
|
+
* @param audience - URI canonique de la ressource visée ; le jeton DOIT la
|
|
141
|
+
* porter dans `aud`
|
|
142
|
+
* @returns le principal établi, ou `null` si le jeton est refusé
|
|
143
|
+
* @throws Error si l'émetteur ne peut pas être joint ou publie un jeu de clés
|
|
144
|
+
* inutilisable — la porte doit alors refuser de servir, pas répondre
|
|
145
|
+
* « jeton invalide »
|
|
146
|
+
*/
|
|
147
|
+
verify(token: string, audience: string): Promise<IAccessPrincipal | null>;
|
|
148
|
+
}
|
|
149
|
+
export default RemoteJwtVerifier;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IUser } from "@nodefony/user";
|
|
2
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
3
|
+
/**
|
|
4
|
+
* Jeton porteur d'un utilisateur réel — produit par les authenticators à
|
|
5
|
+
* credential (`userpassword`, puis `session`/`jwt`...).
|
|
6
|
+
*
|
|
7
|
+
* Cycle en deux états, UN SEUL objet alloué par tentative (cold path login) :
|
|
8
|
+
* 1. `createToken()` → non authentifié : porte le credential brut extrait de la
|
|
9
|
+
* requête, `getUser()` rend l'anonyme (jamais `null`, Zero Trust).
|
|
10
|
+
* 2. `authenticate()` réussit → {@link promote} : l'utilisateur vérifié est posé
|
|
11
|
+
* et le credential est **effacé** (anti-fuite : un mot de passe ne doit
|
|
12
|
+
* survivre ni en mémoire ni dans un heap dump/log).
|
|
13
|
+
*
|
|
14
|
+
* Les attributs (claims, providerId...) sont lazy — `null` tant que rien n'est posé.
|
|
15
|
+
*/
|
|
16
|
+
export declare class UserToken implements IToken {
|
|
17
|
+
#private;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
/**
|
|
20
|
+
* @param type - type du token (`"userpassword"`, `"session"`, `"jwt"`...).
|
|
21
|
+
* @param credentials - credential brut extrait de la requête (vidé au succès).
|
|
22
|
+
*/
|
|
23
|
+
constructor(type: string, credentials?: unknown);
|
|
24
|
+
/**
|
|
25
|
+
* Marque le jeton authentifié : pose l'utilisateur vérifié et EFFACE le
|
|
26
|
+
* credential. Appelé uniquement par l'authenticator au succès.
|
|
27
|
+
*
|
|
28
|
+
* @param user - utilisateur vérifié par la source d'identité.
|
|
29
|
+
* @returns le jeton lui-même (chaînage).
|
|
30
|
+
*/
|
|
31
|
+
promote(user: IUser): this;
|
|
32
|
+
getUser(): IUser;
|
|
33
|
+
getUserIdentifier(): string;
|
|
34
|
+
isAuthenticated(): boolean;
|
|
35
|
+
getRoles(): string[];
|
|
36
|
+
getCredentials(): unknown;
|
|
37
|
+
getScopes(): string[];
|
|
38
|
+
getAttribute<T = unknown>(key: string): T | undefined;
|
|
39
|
+
setAttribute(key: string, value: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
export default UserToken;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* Paramètres JWT **résolus** partagés par l'émetteur ({@link TokenService}) et le
|
|
4
|
+
* vérificateur ({@link JwtAuthenticator}) — garantit que `iss`/`aud` posés à la
|
|
5
|
+
* signature sont EXACTEMENT ceux exigés à la vérification (une divergence = tout
|
|
6
|
+
* rejeté). Fonction pure (pas d'état, pas de kernel) → les deux côtés obtiennent
|
|
7
|
+
* la même valeur sans la partager.
|
|
8
|
+
*/
|
|
9
|
+
export interface IJwtRuntime {
|
|
10
|
+
/** Émetteur (`iss`). */
|
|
11
|
+
readonly issuer: string;
|
|
12
|
+
/** Audiences acceptées au verify ; la première sert d'`aud` à l'émission. */
|
|
13
|
+
readonly audiences: string[];
|
|
14
|
+
/** TTL access token (s). */
|
|
15
|
+
readonly accessTtlS: number;
|
|
16
|
+
/** TTL refresh token (s). */
|
|
17
|
+
readonly refreshTtlS: number;
|
|
18
|
+
/** Rotation du refresh à chaque usage (OWASP / RFC 9700). */
|
|
19
|
+
readonly rotateRefresh: boolean;
|
|
20
|
+
/** Algorithme — `"EdDSA"` (Ed25519). RS256 = slot non câblé en J4a. */
|
|
21
|
+
readonly alg: "EdDSA";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
|
|
25
|
+
* `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
|
|
26
|
+
* propre audience (`[issuer]`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveJwtRuntime(jwt: ISecurityConfig["jwt"]): IJwtRuntime;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Écrire un SECRET sur disque — la seule implémentation du dépôt.
|
|
3
|
+
*
|
|
4
|
+
* ## Les trois règles, et ce que chacune évite
|
|
5
|
+
*
|
|
6
|
+
* 1. **Ne jamais tester la présence avant de lire.** `existsSync(f) ? read(f)
|
|
7
|
+
* : ""` ouvre une fenêtre entre le test et l'usage : le fichier peut
|
|
8
|
+
* disparaître, ou devenir un lien vers ailleurs. La forme juste est de lire
|
|
9
|
+
* et de traiter `ENOENT` — le système de fichiers répond en une opération ce
|
|
10
|
+
* que deux appels ne peuvent pas garantir.
|
|
11
|
+
* 2. **Écrire en 0600, atomiquement.** Un secret créé au masque par défaut est
|
|
12
|
+
* lisible par tous les comptes de la machine, et rien ne le signale. Le
|
|
13
|
+
* couple fichier temporaire + `rename` évite en plus qu'un lecteur tombe sur
|
|
14
|
+
* un fichier à demi écrit.
|
|
15
|
+
* 3. **CONSTATER le mode obtenu.** Le mode demandé est une intention, pas une
|
|
16
|
+
* garantie : NTFS l'ignore, comme un montage FAT/exFAT ou NFS sans mapping
|
|
17
|
+
* d'identité. Une capacité se constate, elle ne se déduit pas de
|
|
18
|
+
* `process.platform` — et si la restriction n'a pas pris, il faut le DIRE
|
|
19
|
+
* plutôt que laisser croire à une protection.
|
|
20
|
+
*
|
|
21
|
+
* ## Pourquoi les deux formes, synchrone et asynchrone
|
|
22
|
+
*
|
|
23
|
+
* Le runtime persiste ses clés dans du code asynchrone ; une commande de CLI
|
|
24
|
+
* écrit un jeton dans un flot synchrone, où introduire une promesse
|
|
25
|
+
* changerait l'ordre des messages affichés. Les deux formes appliquent le même
|
|
26
|
+
* raisonnement, écrit ici une seule fois — deux copies divergeraient, et l'on
|
|
27
|
+
* sait exactement comment : l'une porterait le mode 0600, l'autre non.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
/** Mode attendu d'un fichier qui porte un secret : lisible par son seul propriétaire. */
|
|
32
|
+
export declare const MODE_SECRET = 384;
|
|
33
|
+
/**
|
|
34
|
+
* Le contenu du fichier, ou `null` s'il n'existe pas.
|
|
35
|
+
*
|
|
36
|
+
* @throws Toute erreur autre qu'`ENOENT` — un fichier illisible pour cause de
|
|
37
|
+
* droits n'est PAS un fichier absent, et le confondre ferait écraser un
|
|
38
|
+
* secret existant par un fichier neuf.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readIfPresent(file: string): Promise<string | null>;
|
|
41
|
+
/** Forme synchrone de {@link readIfPresent}. */
|
|
42
|
+
export declare function readIfPresentSync(file: string): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Le mode effectif du fichier n'est-il PAS restreint au propriétaire ?
|
|
45
|
+
*
|
|
46
|
+
* @returns `null` si le fichier a disparu ou n'est pas interrogeable (le chemin
|
|
47
|
+
* d'erreur normal parlera), sinon le mode effectif quand il diffère de
|
|
48
|
+
* 0600 — et `undefined` quand tout va bien.
|
|
49
|
+
*/
|
|
50
|
+
export declare function modeNonRestreint(file: string): number | null | undefined;
|
|
51
|
+
/** Forme asynchrone de {@link modeNonRestreint}. */
|
|
52
|
+
export declare function modeNonRestreintAsync(file: string): Promise<number | null | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* La phrase à journaliser quand la restriction n'a PAS pris.
|
|
55
|
+
*
|
|
56
|
+
* Elle nomme la cause probable et ce qui reste à faire : un avertissement qui
|
|
57
|
+
* dit seulement « mode inattendu » se lit comme du bruit et finit ignoré.
|
|
58
|
+
*/
|
|
59
|
+
export declare function messageNonRestreint(file: string, mode: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* Écrit un secret : dossier créé, mode 0600, remplacement ATOMIQUE.
|
|
62
|
+
*
|
|
63
|
+
* Le mode est posé à la création du temporaire — et non après `rename` — pour
|
|
64
|
+
* qu'il n'existe à aucun instant un fichier au contenu secret et au masque par
|
|
65
|
+
* défaut. `chmod` est ensuite réappliqué sur la cible : un `rename` par-dessus
|
|
66
|
+
* un fichier EXISTANT conserve, sur certains systèmes, le mode de la cible.
|
|
67
|
+
*/
|
|
68
|
+
export declare function writeSecret(file: string, content: string): Promise<void>;
|
|
69
|
+
/** Forme synchrone de {@link writeSecret}. */
|
|
70
|
+
export declare function writeSecretSync(file: string, content: string): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ITokenListQuery } from "../../contracts/ITokenStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fragment de critère **portable** exprimant l'état de vie d'un jeton — la
|
|
4
|
+
* traduction que les adapters SQL et Mongo partagent au lieu de la réécrire.
|
|
5
|
+
*
|
|
6
|
+
* Elle tient dans le `Criteria` d'orm-core depuis que celui-ci porte `$or` :
|
|
7
|
+
* « utilisable » signifie *sans échéance* **ou** *échéance à venir*, ce qu'aucune
|
|
8
|
+
* conjonction ne dit. Avant ça, chaque backend serait descendu à son SQL natif —
|
|
9
|
+
* trois écritures de la même règle, et la divergence pour seule perspective.
|
|
10
|
+
*
|
|
11
|
+
* Révoqué l'emporte sur expiré : les deux autres branches exigent donc
|
|
12
|
+
* explicitement `revokedAt IS NULL`. Sans cette précision, une clé révoquée
|
|
13
|
+
* **puis** échue compterait dans deux facettes, et la somme dépasserait le total.
|
|
14
|
+
*
|
|
15
|
+
* @param status - l'état demandé, ou `undefined` pour ne pas filtrer.
|
|
16
|
+
* @param now - instant de référence (injecté : un compteur ne doit pas dépendre
|
|
17
|
+
* du moment où le test tourne).
|
|
18
|
+
* @returns le fragment à fusionner dans le critère, vide si aucun filtre.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tokenStatusCriteria(status: ITokenListQuery["status"], now: number): Record<string, unknown>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { FacetCounts } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de filtre des jetons**, en noms PUBLICS — ceux qu'un client
|
|
4
|
+
* écrit dans l'URL (`?status=revoked`), jamais des noms de colonne.
|
|
5
|
+
*
|
|
6
|
+
* Frère de `TOKEN_SORTABLE_FIELDS`, et posé pour la même raison : le vocabulaire
|
|
7
|
+
* appartient au propriétaire du contrat (`@nodefony/security`), la mécanique de
|
|
8
|
+
* lecture au cœur (`parseFilters`).
|
|
9
|
+
*
|
|
10
|
+
* **La différence avec le tri est structurelle.** Un tri est une CAPACITÉ de
|
|
11
|
+
* backend — Redis ne sait pas trier, donc `sortableFields` se déclare par store.
|
|
12
|
+
* Un filtre listé ici est une OBLIGATION de tous les backends de jetons : il est
|
|
13
|
+
* inscrit dans {@link ITokenListQuery}, et le store mémoire, SQL, Mongo comme
|
|
14
|
+
* Redis l'honorent chacun à sa façon (`WHERE` indexé, prédicat, filtre inline de
|
|
15
|
+
* batch `SCAN`). Le déclarer par store laisserait croire qu'il est facultatif.
|
|
16
|
+
*
|
|
17
|
+
* **Ce qui n'y est PAS, et pourquoi** : `kind`. Il existe bien au contrat, mais
|
|
18
|
+
* l'endpoint d'administration des clés d'API passe par `listPagePat`, qui impose
|
|
19
|
+
* `kind: "pat"` (`service/apiKeys.ts:210`). L'exposer donnerait un filtre que le
|
|
20
|
+
* service écrase en silence — la faute même que ce chantier corrige.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TOKEN_FILTERS: {
|
|
23
|
+
/** Restreint à un porteur (colonne indexée dans tous les backends SQL). */
|
|
24
|
+
readonly subjectId: "string";
|
|
25
|
+
/**
|
|
26
|
+
* État de vie de la clé — la liste fermée vaut allowlist.
|
|
27
|
+
*
|
|
28
|
+
* Remplace l'ancien `revoked: "boolean"`, qui ne distinguait pas une clé
|
|
29
|
+
* ACTIVE d'une clé ARRIVÉE À ÉCHÉANCE : les deux étaient « non révoquées »,
|
|
30
|
+
* alors que la première ouvre l'accès et la seconde ne l'ouvre plus. La
|
|
31
|
+
* console affichait ces deux populations dans des cartes séparées sans
|
|
32
|
+
* pouvoir les demander au serveur.
|
|
33
|
+
*/
|
|
34
|
+
readonly status: readonly ["active", "expired", "revoked"];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* **Les facettes des jetons** — les questions fermées posées à la collection
|
|
38
|
+
* ENTIÈRE pour les cartes de tête.
|
|
39
|
+
*
|
|
40
|
+
* Contrairement aux webhooks, les trois états **partitionnent** : un jeton est
|
|
41
|
+
* dans exactement une case. On les compte tout de même une par une, sans jamais
|
|
42
|
+
* soustraire — une partition est une propriété du domaine d'aujourd'hui, pas une
|
|
43
|
+
* garantie du code, et un quatrième état la briserait en silence.
|
|
44
|
+
*/
|
|
45
|
+
export declare const TOKEN_FACETS: {
|
|
46
|
+
/** Toutes les clés, quel que soit leur état. */
|
|
47
|
+
readonly total: {};
|
|
48
|
+
/** Utilisables : ni révoquées, ni arrivées à échéance. */
|
|
49
|
+
readonly active: {
|
|
50
|
+
readonly status: "active";
|
|
51
|
+
};
|
|
52
|
+
/** Arrivées à échéance sans avoir été révoquées. */
|
|
53
|
+
readonly expired: {
|
|
54
|
+
readonly status: "expired";
|
|
55
|
+
};
|
|
56
|
+
/** Révoquées par un administrateur. */
|
|
57
|
+
readonly revoked: {
|
|
58
|
+
readonly status: "revoked";
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/** Les compteurs rendus par `GET /nodefony/security/api/apikeys/stats`. */
|
|
62
|
+
export type ITokenCounts = FacetCounts<typeof TOKEN_FACETS>;
|
|
63
|
+
/**
|
|
64
|
+
* Ce que l'endpoint de COMPTEURS accepte de filtrer — `TOKEN_FILTERS` **moins**
|
|
65
|
+
* les champs que les facettes décomposent.
|
|
66
|
+
*
|
|
67
|
+
* `status` en est retiré : le demander à un endpoint dont les cartes SONT les
|
|
68
|
+
* états produirait une réponse qui se contredit — un total suivant le filtre, et
|
|
69
|
+
* chaque facette l'écrasant par le sien. Le refuser dit au client ce qui se
|
|
70
|
+
* passe ; l'accepter lui montrerait « 5 clés, dont 538 révoquées ».
|
|
71
|
+
*
|
|
72
|
+
* Un test verrouille l'accord entre cette liste et {@link TOKEN_FACETS}.
|
|
73
|
+
*/
|
|
74
|
+
export declare const TOKEN_STATS_FILTERS: {
|
|
75
|
+
readonly subjectId: "string";
|
|
76
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IPageQuery } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de tri des jetons**, en noms PUBLICS — ceux qu'un client écrit
|
|
4
|
+
* dans l'URL (`?order=createdAt:DESC`), jamais des noms de colonne.
|
|
5
|
+
*
|
|
6
|
+
* Il vit ici, chez le propriétaire du contrat (`@nodefony/security`), et non dans
|
|
7
|
+
* chaque backend : c'est ce qui garantit qu'une console de clés d'API offre le
|
|
8
|
+
* même tri, que l'application tourne sur mémoire, SQL ou Mongo. Un store dont le
|
|
9
|
+
* schéma nomme un champ autrement traduit **chez lui** — cf
|
|
10
|
+
* {@link translateTokenOrderMongo}.
|
|
11
|
+
*
|
|
12
|
+
* - `createdAt` — date d'émission, l'axe naturel d'une console de clés ;
|
|
13
|
+
* - `name` — le libellé humain, ce qu'on lit dans la colonne de gauche ;
|
|
14
|
+
* - `subjectId` — regroupe les clés d'un même porteur (vue d'administration) ;
|
|
15
|
+
* - `id` — identifiant public, utile surtout en départage.
|
|
16
|
+
*
|
|
17
|
+
* **Ce qui n'y est PAS, et pourquoi** : `lastUsedAt`, `expiresAt` et `revokedAt`
|
|
18
|
+
* sont *nullables*, et le placement des valeurs absentes n'est pas le même d'un
|
|
19
|
+
* moteur à l'autre — PostgreSQL range les `NULL` en tête d'un tri `DESC`, SQLite
|
|
20
|
+
* et MySQL en queue, et le tri en mémoire (`compareByOrder`) les met en queue
|
|
21
|
+
* dans les deux sens. Les déclarer offrirait donc un tri dont l'ordre
|
|
22
|
+
* dépendrait de la base configurée, ce qui est exactement ce que ce vocabulaire
|
|
23
|
+
* existe pour empêcher. Ils s'ouvriront quand la normalisation « absents en
|
|
24
|
+
* queue » sera portée dans le helper de pagination, pas avant.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TOKEN_SORTABLE_FIELDS: readonly ["createdAt", "name", "subjectId", "id"];
|
|
27
|
+
/**
|
|
28
|
+
* Ordre contractuel appliqué quand le client n'en demande aucun : les clés les
|
|
29
|
+
* plus récentes d'abord, départagées par identifiant pour rester **déterministe**
|
|
30
|
+
* à horodatage égal (sans quoi une pagination offset peut sauter ou répéter une
|
|
31
|
+
* ligne entre deux pages).
|
|
32
|
+
*/
|
|
33
|
+
export declare const TOKEN_DEFAULT_ORDER: NonNullable<IPageQuery["order"]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TokenStatus } from "../../contracts/ITokenStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ce qu'il faut d'un jeton pour en déduire l'état — deux horodatages, rien de
|
|
4
|
+
* plus. Volontairement structural : le store mémoire, le batch Redis et un test
|
|
5
|
+
* s'en servent sans partager de type d'enregistrement.
|
|
6
|
+
*/
|
|
7
|
+
export interface ITokenLifetime {
|
|
8
|
+
/** Instant de révocation, ou `null` si jamais révoqué. */
|
|
9
|
+
readonly revokedAt: number | null;
|
|
10
|
+
/** Échéance, ou `null` pour un jeton sans expiration. */
|
|
11
|
+
readonly expiresAt: number | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* **La** définition de l'état d'un jeton — un seul exemplaire, pour les backends
|
|
15
|
+
* qui évaluent en mémoire (store mémoire, filtrage inline d'un batch `SCAN`).
|
|
16
|
+
*
|
|
17
|
+
* L'ordre d'évaluation est significatif : **révoqué l'emporte sur expiré**. Une
|
|
18
|
+
* clé révoquée puis arrivée à échéance reste « révoquée » — c'est l'acte
|
|
19
|
+
* d'administration qui décrit ce qui s'est passé, pas l'écoulement du temps.
|
|
20
|
+
*
|
|
21
|
+
* Les backends SQL et Mongo n'appellent pas cette fonction (ils traduisent la
|
|
22
|
+
* condition dans leur langage, sinon il faudrait rapatrier la collection pour la
|
|
23
|
+
* filtrer) : c'est le banc de contrat partagé qui garantit qu'ils disent la même
|
|
24
|
+
* chose qu'elle.
|
|
25
|
+
*
|
|
26
|
+
* @param token - les deux horodatages du jeton.
|
|
27
|
+
* @param now - l'instant de référence (injecté : les tests ne dépendent pas de
|
|
28
|
+
* l'horloge réelle, et un store porte déjà la sienne).
|
|
29
|
+
*/
|
|
30
|
+
export declare function tokenStatusOf(token: ITokenLifetime, now: number): TokenStatus;
|
|
31
|
+
/**
|
|
32
|
+
* `true` si le jeton correspond au filtre d'état demandé (`undefined` = tous).
|
|
33
|
+
*
|
|
34
|
+
* @param token - les deux horodatages du jeton.
|
|
35
|
+
* @param status - l'état demandé, ou `undefined` pour ne pas filtrer.
|
|
36
|
+
* @param now - l'instant de référence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function matchesTokenStatus(token: ITokenLifetime, status: TokenStatus | undefined, now: number): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
3
|
+
import type { ITokenStore } from "../../contracts/ITokenStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registre de **fabriques de stores de jetons** — résout le nom configuré
|
|
6
|
+
* (`security.tokenStore`) vers une instance d'{@link ITokenStore}, SANS coupler
|
|
7
|
+
* le cœur à un backend en dur.
|
|
8
|
+
*
|
|
9
|
+
* Pourquoi : le store est pluggable par contrat (mémoire/fichier/ORM/Redis) ;
|
|
10
|
+
* un `if (name === "redis")` trahirait cette promesse. Les builtins sans
|
|
11
|
+
* dépendance (`memory`) s'enregistrent au chargement du module ; les adapters
|
|
12
|
+
* lourds (`drizzle`, `mongoose`, `redis`) s'enregistrent depuis LEUR module
|
|
13
|
+
* (inversion de dépendance : ils importent `import type { ITokenStore }`, effacé
|
|
14
|
+
* à la compilation). Convention-frère : `authenticatorRegistry`, `ormRegistry`,
|
|
15
|
+
* `SessionsService.registerStorage`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Contexte passé à une fabrique de store : de quoi se construire (résolutions
|
|
19
|
+
* coûteuses en lazy à l'intérieur de l'instance).
|
|
20
|
+
*/
|
|
21
|
+
export interface ITokenStoreFactoryContext {
|
|
22
|
+
/** Container DI — résolution de services (ORM, redis...). */
|
|
23
|
+
readonly container: Container;
|
|
24
|
+
/** Config sécurité validée + gelée. */
|
|
25
|
+
readonly config: ISecurityConfig;
|
|
26
|
+
}
|
|
27
|
+
/** Fabrique d'un store de jetons pour un nom donné. */
|
|
28
|
+
export type TokenStoreFactory = (ctx: ITokenStoreFactoryContext) => ITokenStore;
|
|
29
|
+
/**
|
|
30
|
+
* Enregistre (ou remplace) la fabrique d'un store de jetons. Appelée par le
|
|
31
|
+
* builtin `memory` au chargement, et par les adapters (drizzle/mongoose/redis)
|
|
32
|
+
* pour les leurs.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerTokenStore(name: string, factory: TokenStoreFactory): void;
|
|
35
|
+
/** Fabrique d'un store par nom, ou `undefined` si inconnu. */
|
|
36
|
+
export declare function getTokenStoreFactory(name: string): TokenStoreFactory | undefined;
|
|
37
|
+
/** Noms enregistrés (validation boot, introspection Studio, tests). */
|
|
38
|
+
export declare function listTokenStores(): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IPage } from "nodefony";
|
|
2
|
+
import type { ITotpSecret } from "../../contracts/ITotpSecret.js";
|
|
3
|
+
import type { ITotpEnrollmentSummary, ITotpListQuery, ITotpSecretStore, TotpSecretUpdate } from "../../contracts/ITotpSecretStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Projette un secret en vue d'introspection — **le seul endroit** où l'on
|
|
6
|
+
* décide ce qui sort d'un store TOTP en mémoire. `secretEnc` et les condensats
|
|
7
|
+
* des codes de récupération n'y figurent pas : seul leur NOMBRE est exposé.
|
|
8
|
+
*
|
|
9
|
+
* @param secret - le secret stocké.
|
|
10
|
+
* @returns la vue publique de l'enrôlement.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toTotpEnrollment(secret: ITotpSecret): ITotpEnrollmentSummary;
|
|
13
|
+
/** Applique les filtres d'{@link ITotpListQuery} — sémantique de RÉFÉRENCE. */
|
|
14
|
+
export declare function matchesTotpQuery(secret: ITotpSecret, query: ITotpListQuery): boolean;
|
|
15
|
+
/** Instantané sérialisable de l'état — base de la persistance fichier. */
|
|
16
|
+
export interface TotpStoreSnapshot {
|
|
17
|
+
secrets: ITotpSecret[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Store de secrets TOTP **en mémoire** — implémentation de référence
|
|
21
|
+
* d'{@link ITotpSecretStore}. Clé = `userId` (un secret par utilisateur).
|
|
22
|
+
*
|
|
23
|
+
* 0 dépendance, idéale pour le développement mono-process et les **tests**. NON
|
|
24
|
+
* partagée entre process et **volatile** → en production multi-process, utiliser
|
|
25
|
+
* un adapter ORM ou Redis. Perf : la `Map` n'existe que si le store est instancié
|
|
26
|
+
* (2FA activé), jamais sur le hot path par requête.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MemoryTotpSecretStore implements ITotpSecretStore {
|
|
29
|
+
#private;
|
|
30
|
+
findByUser(userId: string): Promise<ITotpSecret | null>;
|
|
31
|
+
save(secret: ITotpSecret): Promise<void>;
|
|
32
|
+
update(userId: string, patch: TotpSecretUpdate): Promise<void>;
|
|
33
|
+
delete(userId: string): Promise<void>;
|
|
34
|
+
/** {@inheritDoc ITotpSecretStore.listPage} */
|
|
35
|
+
listPage(query: ITotpListQuery): Promise<IPage<ITotpEnrollmentSummary>>;
|
|
36
|
+
/** {@inheritDoc ITotpSecretStore.countEnrollments} */
|
|
37
|
+
countEnrollments(query: ITotpListQuery): Promise<number>;
|
|
38
|
+
/** Instantané sérialisable de l'état courant (pour la persistance fichier). */
|
|
39
|
+
snapshot(): TotpStoreSnapshot;
|
|
40
|
+
/** Remplace l'état par celui d'un instantané. */
|
|
41
|
+
restore(snapshot: TotpStoreSnapshot): void;
|
|
42
|
+
}
|
|
43
|
+
export default MemoryTotpSecretStore;
|