@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +48 -16
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
- package/dist/nodefony/command/security-oauth-doctor.js +68 -0
- package/dist/nodefony/command/security-secrets.js +52 -29
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +10 -7
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +9 -14
- package/dist/nodefony/config/config.js +99 -8
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
- package/dist/nodefony/scaffold/contribute.js +19 -0
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +118 -16
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +116 -25
- package/dist/nodefony/service/keycloak.js +208 -0
- package/dist/nodefony/service/oauth2.js +403 -28
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +94 -27
- package/dist/nodefony/service/totp.js +55 -11
- package/dist/nodefony/service/webAuthn.js +24 -7
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +15 -5
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -3
- package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +38 -0
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +28 -12
- package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
- package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
- package/dist/nodefony/src/oauth/providerRoles.js +78 -0
- package/dist/nodefony/src/oauth/providers/github.js +48 -12
- package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
- package/dist/nodefony/src/openToSignup.js +46 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +25 -0
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +89 -48
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +60 -4
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +64 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +145 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +182 -175
- package/docs/audit.md +141 -141
- package/docs/authenticators.md +129 -96
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +77 -72
- package/docs/external-jwt.md +83 -11
- package/docs/firewall.md +119 -83
- package/docs/headers.md +82 -82
- package/docs/index.md +16 -8
- package/docs/keycloak.md +835 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +291 -166
- package/docs/obtenir-un-jeton.md +131 -34
- package/docs/tokens.md +197 -140
- package/docs/totp.md +177 -136
- package/docs/webauthn.md +157 -154
- package/docs/webhooks.md +128 -125
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +19 -17
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -3,7 +3,7 @@ import { AUDIT_FILTERS } from "../audit/auditFilters.js";
|
|
|
3
3
|
import { adminActor, auditAdmin } from "./adminAudit.js";
|
|
4
4
|
import { webhookAdminEndpoints } from "./WebhookAdminApi.js";
|
|
5
5
|
import { parseFilters, parsePageQuery } from "nodefony";
|
|
6
|
-
|
|
6
|
+
// nodefony/src/admin/SecurityAdminApi.ts
|
|
7
7
|
/**
|
|
8
8
|
* Déduit le **driver** logique d'un store à partir de son nom de classe — le
|
|
9
9
|
* `tokenService` connaît le driver config (`tokenStore.store`) mais ne l'expose
|
|
@@ -99,6 +99,39 @@ function parseAuditQuery(query) {
|
|
|
99
99
|
if (page.cursor !== void 0) filter.cursor = page.cursor;
|
|
100
100
|
return filter;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Traduit l'`id` d'un compte (paramètre `{id}` d'une route admin) en sa **clé de
|
|
104
|
+
* sujet** `IUser.identifier` — la clé sous laquelle vivent ses facteurs (TOTP,
|
|
105
|
+
* passkeys), sa session et ses jetons.
|
|
106
|
+
*
|
|
107
|
+
* Viser un store avec l'`id` brut agit sur une clé qui n'existe pas et rend un
|
|
108
|
+
* succès : l'administrateur croit avoir débloqué un compte resté bloqué. D'où
|
|
109
|
+
* le 503 quand le service `users` manque — sans lui, l'`id` ne se traduit pas.
|
|
110
|
+
*
|
|
111
|
+
* @param container - container du kernel (service `users`).
|
|
112
|
+
* @param id - paramètre `{id}` de la route.
|
|
113
|
+
* @returns le compte (`id` + `identifier`), ou la réponse d'erreur à rendre (404/503).
|
|
114
|
+
*/
|
|
115
|
+
async function resolveSubject(container, id) {
|
|
116
|
+
if (typeof id !== "string" || id.length === 0) return {
|
|
117
|
+
status: 404,
|
|
118
|
+
body: { error: "not found" }
|
|
119
|
+
};
|
|
120
|
+
const users = container.get("users");
|
|
121
|
+
if (!users) return {
|
|
122
|
+
status: 503,
|
|
123
|
+
body: { error: "users unavailable" }
|
|
124
|
+
};
|
|
125
|
+
const user = await users.findById(id);
|
|
126
|
+
if (!user || typeof user.identifier !== "string") return {
|
|
127
|
+
status: 404,
|
|
128
|
+
body: { error: "not found" }
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
identifier: user.identifier
|
|
133
|
+
};
|
|
134
|
+
}
|
|
102
135
|
/** Projette un credential serveur vers sa vue admin (sans `publicKey`/`userId`). */
|
|
103
136
|
function toCredentialView(c) {
|
|
104
137
|
return {
|
|
@@ -170,6 +203,25 @@ function createSecurityAdminApi(container) {
|
|
|
170
203
|
return firewall.describe();
|
|
171
204
|
}
|
|
172
205
|
},
|
|
206
|
+
{
|
|
207
|
+
path: "oauth/diagnosis",
|
|
208
|
+
method: "GET",
|
|
209
|
+
role: "ROLE_NODEFONY_ADMIN",
|
|
210
|
+
summary: "Diagnostic des fournisseurs OAuth sans connexion humaine : émetteur, URL de retour, secret (?provider=<nom>). Interroge les fournisseurs à chaque appel ; aucun secret rendu.",
|
|
211
|
+
handler: async (request) => {
|
|
212
|
+
const oauth = container.get("oauth2");
|
|
213
|
+
if (!oauth || !oauth.isEnabled()) return {
|
|
214
|
+
enabled: false,
|
|
215
|
+
providers: []
|
|
216
|
+
};
|
|
217
|
+
const raw = request.query.provider;
|
|
218
|
+
const provider = typeof raw === "string" && raw !== "" ? raw : void 0;
|
|
219
|
+
return {
|
|
220
|
+
enabled: true,
|
|
221
|
+
providers: await oauth.diagnose(provider)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
173
225
|
{
|
|
174
226
|
path: "roleHierarchy",
|
|
175
227
|
method: "GET",
|
|
@@ -324,17 +376,9 @@ function createSecurityAdminApi(container) {
|
|
|
324
376
|
status: 503,
|
|
325
377
|
body: { error: "webauthn unavailable" }
|
|
326
378
|
};
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
body: { error: "not found" }
|
|
331
|
-
};
|
|
332
|
-
const users = container.get("users");
|
|
333
|
-
if (users && !await users.findById(id)) return {
|
|
334
|
-
status: 404,
|
|
335
|
-
body: { error: "not found" }
|
|
336
|
-
};
|
|
337
|
-
return { credentials: (await svc.listUserCredentials(id)).map(toCredentialView) };
|
|
379
|
+
const account = await resolveSubject(container, request.params.id);
|
|
380
|
+
if (!("identifier" in account)) return account;
|
|
381
|
+
return { credentials: (await svc.listUserCredentials(account.identifier)).map(toCredentialView) };
|
|
338
382
|
}
|
|
339
383
|
},
|
|
340
384
|
{
|
|
@@ -348,13 +392,14 @@ function createSecurityAdminApi(container) {
|
|
|
348
392
|
status: 503,
|
|
349
393
|
body: { error: "webauthn unavailable" }
|
|
350
394
|
};
|
|
351
|
-
const id = request.params.id;
|
|
352
395
|
const credentialId = request.params.credentialId;
|
|
353
|
-
if (typeof
|
|
396
|
+
if (typeof credentialId !== "string" || credentialId.length === 0) return {
|
|
354
397
|
status: 404,
|
|
355
398
|
body: { error: "not found" }
|
|
356
399
|
};
|
|
357
|
-
|
|
400
|
+
const account = await resolveSubject(container, request.params.id);
|
|
401
|
+
if (!("identifier" in account)) return account;
|
|
402
|
+
if (!await svc.removeUserCredential(account.identifier, credentialId)) return {
|
|
358
403
|
status: 404,
|
|
359
404
|
body: { error: "not found" }
|
|
360
405
|
};
|
|
@@ -363,7 +408,7 @@ function createSecurityAdminApi(container) {
|
|
|
363
408
|
action: "user.passkey_revoked",
|
|
364
409
|
outcome: "success",
|
|
365
410
|
actor: adminActor(request.user),
|
|
366
|
-
resource: id,
|
|
411
|
+
resource: account.id,
|
|
367
412
|
metadata: {
|
|
368
413
|
credentialId,
|
|
369
414
|
viaAdmin: true
|
|
@@ -381,7 +426,8 @@ function createSecurityAdminApi(container) {
|
|
|
381
426
|
const svc = container.get("totp");
|
|
382
427
|
const pageQuery = parsePageQuery(request.query, {
|
|
383
428
|
defaultLimit: KEYS_DEFAULT_LIMIT,
|
|
384
|
-
maxLimit: KEYS_MAX_LIMIT
|
|
429
|
+
maxLimit: KEYS_MAX_LIMIT,
|
|
430
|
+
searchable: true
|
|
385
431
|
});
|
|
386
432
|
const limit = pageQuery.limit;
|
|
387
433
|
const offset = pageQuery.offset ?? 0;
|
|
@@ -393,7 +439,7 @@ function createSecurityAdminApi(container) {
|
|
|
393
439
|
offset
|
|
394
440
|
};
|
|
395
441
|
const confirmed = one(request.query, "confirmed");
|
|
396
|
-
const q =
|
|
442
|
+
const q = pageQuery.q;
|
|
397
443
|
const page = await svc.listPage({
|
|
398
444
|
limit,
|
|
399
445
|
offset,
|
|
@@ -420,17 +466,9 @@ function createSecurityAdminApi(container) {
|
|
|
420
466
|
status: 503,
|
|
421
467
|
body: { error: "totp unavailable" }
|
|
422
468
|
};
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
body: { error: "not found" }
|
|
427
|
-
};
|
|
428
|
-
const users = container.get("users");
|
|
429
|
-
if (users && !await users.findById(id)) return {
|
|
430
|
-
status: 404,
|
|
431
|
-
body: { error: "not found" }
|
|
432
|
-
};
|
|
433
|
-
return svc.status(id);
|
|
469
|
+
const account = await resolveSubject(container, request.params.id);
|
|
470
|
+
if (!("identifier" in account)) return account;
|
|
471
|
+
return svc.status(account.identifier);
|
|
434
472
|
}
|
|
435
473
|
},
|
|
436
474
|
{
|
|
@@ -444,23 +482,15 @@ function createSecurityAdminApi(container) {
|
|
|
444
482
|
status: 503,
|
|
445
483
|
body: { error: "totp unavailable" }
|
|
446
484
|
};
|
|
447
|
-
const
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
body: { error: "not found" }
|
|
451
|
-
};
|
|
452
|
-
const users = container.get("users");
|
|
453
|
-
if (users && !await users.findById(id)) return {
|
|
454
|
-
status: 404,
|
|
455
|
-
body: { error: "not found" }
|
|
456
|
-
};
|
|
457
|
-
await svc.disable(id);
|
|
485
|
+
const account = await resolveSubject(container, request.params.id);
|
|
486
|
+
if (!("identifier" in account)) return account;
|
|
487
|
+
await svc.disable(account.identifier);
|
|
458
488
|
auditAdmin(container, {
|
|
459
489
|
category: "auth",
|
|
460
490
|
action: "user.totp_disabled",
|
|
461
491
|
outcome: "success",
|
|
462
492
|
actor: adminActor(request.user),
|
|
463
|
-
resource: id,
|
|
493
|
+
resource: account.id,
|
|
464
494
|
metadata: { viaAdmin: true }
|
|
465
495
|
});
|
|
466
496
|
return { ok: true };
|
|
@@ -492,5 +522,4 @@ function registerSecurityAdminApi(registry, container) {
|
|
|
492
522
|
if (registry.has("security")) return;
|
|
493
523
|
registry.register(createSecurityAdminApi(container));
|
|
494
524
|
}
|
|
495
|
-
//#endregion
|
|
496
525
|
export { createSecurityAdminApi, parseAuditQuery, parseTokenListQuery, registerSecurityAdminApi };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WEBHOOK_FACETS, WEBHOOK_FILTERS, WEBHOOK_STATS_FILTERS } from "../webhook/webhookFilters.js";
|
|
2
2
|
import { adminActor, auditAdmin } from "./adminAudit.js";
|
|
3
|
-
import { parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
-
|
|
3
|
+
import { isPlainObject, parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
+
// nodefony/src/admin/WebhookAdminApi.ts
|
|
5
5
|
/**
|
|
6
6
|
* Déduit le driver logique du store webhook depuis son nom de classe (miroir de
|
|
7
7
|
* `tokenStoreDriver`). Fermé sur les implémentations connues ; `null` pour un
|
|
@@ -30,6 +30,21 @@ function bodyStringArray(v) {
|
|
|
30
30
|
if (!Array.isArray(v) || v.length === 0) return void 0;
|
|
31
31
|
return v.every((x) => typeof x === "string" && x.length > 0) ? v : void 0;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Le corps d'une requête d'écriture : un objet JSON, ou rien (lu comme `{}`).
|
|
35
|
+
* Un tableau ou un scalaire rend `null` — l'appelant répond 400 au lieu de
|
|
36
|
+
* laisser un `in` lever sur une chaîne.
|
|
37
|
+
*/
|
|
38
|
+
function bodyObject(request) {
|
|
39
|
+
const body = request.body;
|
|
40
|
+
if (body === void 0 || body === null) return {};
|
|
41
|
+
return isPlainObject(body) ? body : null;
|
|
42
|
+
}
|
|
43
|
+
/** Réponse au corps qui n'est pas un objet JSON. */
|
|
44
|
+
const BODY_NOT_OBJECT = {
|
|
45
|
+
status: 400,
|
|
46
|
+
body: { error: "JSON object body required" }
|
|
47
|
+
};
|
|
33
48
|
/** Page par défaut du listing d'endpoints (registre de config, volume modeste). */
|
|
34
49
|
const ENDPOINTS_DEFAULT_LIMIT = 50;
|
|
35
50
|
/** Plafond dur : un client ne peut pas demander « tout » via `?limit=`. */
|
|
@@ -75,8 +90,8 @@ function parseWebhookListQuery(query, sortable) {
|
|
|
75
90
|
* fail-closed, et logge).
|
|
76
91
|
*/
|
|
77
92
|
function mapWebhookError(e) {
|
|
78
|
-
if (e.code === 422) {
|
|
79
|
-
const message = e.message;
|
|
93
|
+
if (typeof e === "object" && e !== null && "code" in e && e.code === 422) {
|
|
94
|
+
const message = "message" in e ? e.message : void 0;
|
|
80
95
|
return {
|
|
81
96
|
status: 422,
|
|
82
97
|
body: { error: typeof message === "string" ? message : "invalid url" }
|
|
@@ -170,7 +185,8 @@ function webhookAdminEndpoints(container) {
|
|
|
170
185
|
handler: async (request) => {
|
|
171
186
|
const s = svc();
|
|
172
187
|
if (!ready(s)) return UNAVAILABLE;
|
|
173
|
-
const body = request
|
|
188
|
+
const body = bodyObject(request);
|
|
189
|
+
if (body === null) return BODY_NOT_OBJECT;
|
|
174
190
|
const url = bodyString(body.url);
|
|
175
191
|
if (!url) return {
|
|
176
192
|
status: 400,
|
|
@@ -252,7 +268,8 @@ function webhookAdminEndpoints(container) {
|
|
|
252
268
|
if (!ready(s)) return UNAVAILABLE;
|
|
253
269
|
const id = pathId(request);
|
|
254
270
|
if (!id) return NOT_FOUND;
|
|
255
|
-
const body = request
|
|
271
|
+
const body = bodyObject(request);
|
|
272
|
+
if (body === null) return BODY_NOT_OBJECT;
|
|
256
273
|
const patch = {};
|
|
257
274
|
const fields = [];
|
|
258
275
|
if ("url" in body) {
|
|
@@ -375,5 +392,4 @@ function webhookAdminEndpoints(container) {
|
|
|
375
392
|
}
|
|
376
393
|
];
|
|
377
394
|
}
|
|
378
|
-
//#endregion
|
|
379
395
|
export { parseWebhookListQuery, webhookAdminEndpoints };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/admin/adminAudit.ts
|
|
2
2
|
/**
|
|
3
3
|
* Helpers d'audit PARTAGÉS par les producteurs admin du module sécurité
|
|
4
4
|
* (`SecurityAdminApi`, `WebhookAdminApi`…). Extraits dans leur propre fichier
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
function adminActor(user) {
|
|
17
17
|
if (user && typeof user === "object") {
|
|
18
18
|
const u = user;
|
|
19
|
-
if (typeof u.username === "string" && u.username) return u.username;
|
|
20
19
|
if (typeof u.identifier === "string" && u.identifier) return u.identifier;
|
|
21
20
|
}
|
|
22
21
|
return "admin";
|
|
@@ -33,5 +32,4 @@ function adminActor(user) {
|
|
|
33
32
|
function auditAdmin(container, draft) {
|
|
34
33
|
container.get("auditService")?.record?.(draft);
|
|
35
34
|
}
|
|
36
|
-
//#endregion
|
|
37
35
|
export { adminActor, auditAdmin };
|
|
@@ -1,40 +1,168 @@
|
|
|
1
|
-
import { USER_REVOKED_EVENT } from "@nodefony/user";
|
|
2
|
-
|
|
1
|
+
import { USER_CREATED_EVENT, USER_REVOKED_EVENT } from "@nodefony/user";
|
|
2
|
+
// nodefony/src/admin/userRevocationCascade.ts
|
|
3
|
+
/** Garde : la valeur sait-elle journaliser (`log` est une fonction) ? */
|
|
4
|
+
function isKernelLogger(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && "log" in value && typeof value.log === "function";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Journalise l'échec d'une brique de la cascade. Le geste reste best-effort —
|
|
9
|
+
* une brique en panne n'empêche pas les autres — mais il ne peut plus échouer
|
|
10
|
+
* EN SILENCE : un accès qui survit à sa révocation (session encore ouverte
|
|
11
|
+
* après un changement de mot de passe) doit laisser une trace qui dit laquelle.
|
|
12
|
+
*
|
|
13
|
+
* @param container - container du kernel (service `kernel`).
|
|
14
|
+
* @param brick - brique en échec (`sessions`, `tokenStore`, `totp`, `webauthn`).
|
|
15
|
+
* @param identifier - compte visé par la cascade.
|
|
16
|
+
* @param error - erreur levée par la brique.
|
|
17
|
+
*/
|
|
18
|
+
function reportCascadeFailure(container, brick, identifier, error) {
|
|
19
|
+
try {
|
|
20
|
+
const kernel = container.get("kernel");
|
|
21
|
+
if (!isKernelLogger(kernel)) return;
|
|
22
|
+
kernel.log(`révocation de "${identifier}" incomplète : la brique ${brick} a échoué — ` + (error instanceof Error ? error.message : String(error)), "ERROR", "SECURITY");
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
3
25
|
/**
|
|
4
26
|
* Cascade de révocation déclenchée par {@link USER_REVOKED_EVENT} : éjecte
|
|
5
27
|
* **immédiatement** les artefacts d'accès du porteur — ses **sessions** (http,
|
|
6
28
|
* `destroyByUser`) et ses **jetons/PAT** (`tokenStore.revokeAllForSubject`,
|
|
7
|
-
* seuil `invalidBefore
|
|
8
|
-
* l'autre) — l'accès était DÉJÀ neutralisé par
|
|
9
|
-
*
|
|
10
|
-
*
|
|
29
|
+
* seuil `invalidBefore`, qui couvre aussi les clés d'API). Best-effort par
|
|
30
|
+
* brique (une indispo n'empêche pas l'autre) — l'accès était DÉJÀ neutralisé par
|
|
31
|
+
* le re-fetch des authenticators. `tenantId` du payload est réservé (scoping non
|
|
32
|
+
* câblé en mono-tenant).
|
|
33
|
+
*
|
|
34
|
+
* **Suppression du compte** (`reason: "deleted"`) : retire AUSSI ses facteurs —
|
|
35
|
+
* secret TOTP (codes de récupération compris) et passkeys. Ils sont rangés sous
|
|
36
|
+
* l'`identifier`, qu'un compte recréé peut reprendre : laissés en place, le
|
|
37
|
+
* nouveau titulaire hériterait du second facteur de l'ancien. Un compte
|
|
38
|
+
* seulement désactivé ou verrouillé les GARDE — il les retrouve à sa
|
|
39
|
+
* réactivation.
|
|
11
40
|
*
|
|
12
|
-
* @param container - container du kernel (résolution lazy de `sessions
|
|
41
|
+
* @param container - container du kernel (résolution lazy de `sessions`, `tokenStore`, `totp`, `webauthn`).
|
|
13
42
|
* @param event - charge utile de l'événement (porteur + raison).
|
|
14
43
|
* @param now - horloge (epoch ms) injectable pour les tests.
|
|
15
44
|
*/
|
|
16
45
|
async function cascadeUserRevocation(container, event, now = Date.now()) {
|
|
17
46
|
const identifier = event.identifier;
|
|
47
|
+
await revokeAccessArtifacts(container, identifier, now);
|
|
48
|
+
if (event.reason === "deleted") await removeAccountFactors(container, identifier);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Éjecte les artefacts d'accès rangés sous un identifiant — ses sessions (http,
|
|
52
|
+
* `destroyByUser`) et ses jetons/PAT/clés d'API (`tokenStore.revokeAllForSubject`,
|
|
53
|
+
* seuil `invalidBefore`). SEULE implémentation du geste, appelée à la révocation
|
|
54
|
+
* d'un compte ET à sa création. Best-effort par brique : l'indisponibilité de
|
|
55
|
+
* l'une n'empêche pas l'autre.
|
|
56
|
+
*
|
|
57
|
+
* @param container - container du kernel (services `sessions`, `tokenStore`).
|
|
58
|
+
* @param identifier - `IUser.identifier` du compte.
|
|
59
|
+
* @param invalidBefore - seuil (epoch ms) : tout jeton émis avant est rejeté.
|
|
60
|
+
*/
|
|
61
|
+
async function revokeAccessArtifacts(container, identifier, invalidBefore) {
|
|
18
62
|
try {
|
|
19
63
|
await container.get("sessions")?.destroyByUser?.(identifier);
|
|
20
|
-
} catch {
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reportCascadeFailure(container, "sessions", identifier, error);
|
|
66
|
+
}
|
|
21
67
|
try {
|
|
22
|
-
await container.get("tokenStore")?.revokeAllForSubject(identifier,
|
|
23
|
-
} catch {
|
|
68
|
+
await container.get("tokenStore")?.revokeAllForSubject(identifier, invalidBefore);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
reportCascadeFailure(container, "tokenStore", identifier, error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Purge, à la NAISSANCE d'un compte, tout ce qu'un ancien titulaire du même
|
|
75
|
+
* identifiant a laissé derrière lui : sessions, jetons et facteurs. Un compte
|
|
76
|
+
* qui vient de naître n'a encore rien émis — tout artefact déjà rangé sous son
|
|
77
|
+
* identifiant appartient donc à quelqu'un d'autre. C'est ce qui ferme le cas
|
|
78
|
+
* d'un compte supprimé HORS du framework (`DELETE` SQL, aucun événement), dont
|
|
79
|
+
* l'accès redeviendrait valide dès qu'un compte renaît sous son nom.
|
|
80
|
+
*
|
|
81
|
+
* Le seuil est tronqué à la SECONDE : l'`iat` d'un JWT l'est aussi (RFC 7519
|
|
82
|
+
* §2, NumericDate), et un seuil à la milliseconde rejetterait le jeton du
|
|
83
|
+
* nouveau titulaire émis dans la même seconde que sa création.
|
|
84
|
+
*
|
|
85
|
+
* @param container - container du kernel.
|
|
86
|
+
* @param identifier - `IUser.identifier` du compte créé.
|
|
87
|
+
* @param now - horloge (epoch ms) injectable pour les tests.
|
|
88
|
+
*/
|
|
89
|
+
async function purgeInheritedAccess(container, identifier, now = Date.now()) {
|
|
90
|
+
await revokeAccessArtifacts(container, identifier, Math.floor(now / 1e3) * 1e3);
|
|
91
|
+
await removeAccountFactors(container, identifier);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Retire les facteurs rangés sous un identifiant — secret TOTP (codes de
|
|
95
|
+
* récupération compris) et passkeys. SEULE implémentation du geste, appelée à
|
|
96
|
+
* la suppression d'un compte ET à la création d'un compte sous cet identifiant.
|
|
97
|
+
* Best-effort par brique : un TOTP indisponible n'empêche pas le retrait des
|
|
98
|
+
* passkeys.
|
|
99
|
+
*
|
|
100
|
+
* @param container - container du kernel (services `totp`, `webauthn`).
|
|
101
|
+
* @param identifier - `IUser.identifier` du compte.
|
|
102
|
+
*/
|
|
103
|
+
async function removeAccountFactors(container, identifier) {
|
|
104
|
+
try {
|
|
105
|
+
const totp = container.get("totp");
|
|
106
|
+
if (totp?.isEnabled?.() !== false) await totp?.disable?.(identifier);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
reportCascadeFailure(container, "totp", identifier, error);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const webauthn = container.get("webauthn");
|
|
112
|
+
if (webauthn?.isEnabled?.() !== false) {
|
|
113
|
+
const creds = await webauthn?.listUserCredentials?.(identifier) ?? [];
|
|
114
|
+
for (const cred of creds) await webauthn?.removeUserCredential?.(identifier, cred.id);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
reportCascadeFailure(container, "webauthn", identifier, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Garde : charge utile de {@link USER_CREATED_EVENT} (identifiant non vide). */
|
|
121
|
+
function isUserCreatedEvent(value) {
|
|
122
|
+
return typeof value === "object" && value !== null && "identifier" in value && typeof value.identifier === "string" && value.identifier.length > 0;
|
|
24
123
|
}
|
|
25
124
|
/**
|
|
26
|
-
* Abonne la cascade au bus kernel
|
|
27
|
-
*
|
|
28
|
-
* (
|
|
29
|
-
*
|
|
125
|
+
* Abonne la cascade au bus kernel, UNE fois, au `onKernelBoot` de
|
|
126
|
+
* `@nodefony/security` — avant qu'aucun compte ne puisse être créé ou révoqué
|
|
127
|
+
* (les comptes semés au démarrage compris). Deux abonnements :
|
|
128
|
+
* - {@link USER_REVOKED_EVENT} → {@link cascadeUserRevocation} ;
|
|
129
|
+
* - {@link USER_CREATED_EVENT} → {@link purgeInheritedAccess} : sessions,
|
|
130
|
+
* jetons et facteurs HÉRITÉS. Un compte qui vient de naître n'a encore rien
|
|
131
|
+
* émis ni enrôlé, donc tout artefact déjà rangé sous son identifiant
|
|
132
|
+
* appartient à un ancien titulaire. Règle exacte, aucune date à comparer.
|
|
133
|
+
* ⚠️ Un import qui chargerait les facteurs AVANT les comptes les verrait
|
|
134
|
+
* retirés : importer les comptes d'abord.
|
|
135
|
+
*
|
|
136
|
+
* Les deux sont émis par `UserService` seul, et attendus : chaque handler rend
|
|
137
|
+
* sa promesse. **Extensible** : tout autre module (webhooks…) s'abonne aux
|
|
138
|
+
* MÊMES événements pour ses propres artefacts, sans toucher à ce fichier.
|
|
30
139
|
*
|
|
31
140
|
* @param kernel - bus d'événements (kernel) exposant `on`.
|
|
32
|
-
* @param container - container capturé par
|
|
141
|
+
* @param container - container capturé par les handlers.
|
|
33
142
|
*/
|
|
34
143
|
function registerUserRevocationCascade(kernel, container) {
|
|
35
|
-
kernel.on(USER_REVOKED_EVENT, (event) =>
|
|
36
|
-
|
|
37
|
-
|
|
144
|
+
kernel.on(USER_REVOKED_EVENT, (event) => cascadeUserRevocation(container, event));
|
|
145
|
+
kernel.on(USER_CREATED_EVENT, (event) => isUserCreatedEvent(event) ? purgeInheritedAccess(container, event.identifier) : void 0);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Avis de démarrage quand le service `users` n'émet pas sur le bus du kernel —
|
|
149
|
+
* construit sans container (`new UserService(repo, encoder)`). Ses événements
|
|
150
|
+
* partent alors sur son propre bus, où personne n'écoute : supprimer un compte
|
|
151
|
+
* laisserait ses sessions, ses jetons et ses facteurs en place, SANS UN MOT.
|
|
152
|
+
*
|
|
153
|
+
* @param container - container du kernel.
|
|
154
|
+
* @param kernel - le kernel attendu comme bus des événements.
|
|
155
|
+
* @returns l'avis à déclarer, ou `null` si `users` est absent ou bien câblé.
|
|
156
|
+
*/
|
|
157
|
+
function usersEventBusNotice(container, kernel) {
|
|
158
|
+
const users = container.get("users");
|
|
159
|
+
if (typeof users !== "object" || users === null) return null;
|
|
160
|
+
if (!("kernel" in users) || users.kernel === kernel) return null;
|
|
161
|
+
return {
|
|
162
|
+
code: "USERS_SERVICE_WITHOUT_KERNEL",
|
|
163
|
+
level: "warning",
|
|
164
|
+
message: "le service « users » n'atteint pas le kernel : supprimer, désactiver un compte ou changer son mot de passe ne révoquera ni ses sessions, ni ses jetons, ni ses facteurs.",
|
|
165
|
+
fix: "new UserService(repository, encoder, module.container)"
|
|
166
|
+
};
|
|
38
167
|
}
|
|
39
|
-
|
|
40
|
-
export { cascadeUserRevocation, registerUserRevocationCascade };
|
|
168
|
+
export { cascadeUserRevocation, purgeInheritedAccess, registerUserRevocationCascade, removeAccountFactors, usersEventBusNotice };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/apikey/apiKeyFormat.ts
|
|
3
3
|
/**
|
|
4
4
|
* Encodage / décodage / hachage des clés API (PAT) — logique **pure** (aucun I/O,
|
|
5
5
|
* aucun container) → testable sans serveur et partagée par l'émetteur
|
|
@@ -103,5 +103,4 @@ function parseApiKey(token, prefix) {
|
|
|
103
103
|
secretHash: hashApiKey(token)
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
-
//#endregion
|
|
107
106
|
export { generateApiKey, hashApiKey, looksLikeApiKey, parseApiKey };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertPageQuery } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/audit/MemoryAuditStore.ts
|
|
3
3
|
const DEFAULT_MAX_ENTRIES = 1e4;
|
|
4
4
|
const MAX_LIMIT = 500;
|
|
5
5
|
/**
|
|
@@ -113,5 +113,4 @@ var MemoryAuditStore = class {
|
|
|
113
113
|
for (const event of snapshot.events) this.#events.push(event);
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
|
-
//#endregion
|
|
117
116
|
export { MemoryAuditStore };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PLATFORM_CHANNELS } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/audit/auditBridge.ts
|
|
3
3
|
/**
|
|
4
4
|
* Canal WS du flux live d'audit (P6.14 lot 4). Le préfixe `security:` le place
|
|
5
5
|
* sous le plancher `SECURITY_CHANNEL_POLICY` (ROLE_NODEFONY_ADMIN) du verrou de
|
|
@@ -82,5 +82,4 @@ function createAuditBridge(source, publish, channel, opts = {}) {
|
|
|
82
82
|
unsubscribe();
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
//#endregion
|
|
86
85
|
export { SECURITY_AUDIT_CHANNEL, createAuditBridge, createAuditBridge as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/audit/auditFilters.ts
|
|
2
2
|
/**
|
|
3
3
|
* **Le vocabulaire de filtre du journal d'audit**, en noms PUBLICS — ceux qu'un
|
|
4
4
|
* auditeur écrit dans l'URL (`?category=authz&outcome=denied&since=…`).
|
|
@@ -56,5 +56,4 @@ const AUDIT_FILTERS = {
|
|
|
56
56
|
* les deux à jour », qui avait échoué en silence.
|
|
57
57
|
*/
|
|
58
58
|
const AUDIT_FILTER_VOCABULARY_IS_COMPLETE = [true, true];
|
|
59
|
-
//#endregion
|
|
60
59
|
export { AUDIT_FILTERS, AUDIT_FILTER_VOCABULARY_IS_COMPLETE };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemoryAuditStore } from "./MemoryAuditStore.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/audit/auditStoreRegistry.ts
|
|
3
3
|
const factories = /* @__PURE__ */ new Map();
|
|
4
4
|
/**
|
|
5
5
|
* Enregistre (ou remplace) la fabrique d'un store d'audit. Appelée par le builtin
|
|
@@ -21,5 +21,4 @@ registerAuditStore("memory", (ctx) => {
|
|
|
21
21
|
const days = ctx?.config?.audit?.retentionDays;
|
|
22
22
|
return typeof days === "number" ? new MemoryAuditStore(Date.now, days * MS_PER_DAY) : new MemoryAuditStore();
|
|
23
23
|
});
|
|
24
|
-
//#endregion
|
|
25
24
|
export { getAuditStoreFactory, listAuditStores, registerAuditStore };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/audit/readAuditContext.ts
|
|
2
2
|
/**
|
|
3
3
|
* Extrait IP / User-Agent / requestId + drapeaux de présence (jamais la valeur)
|
|
4
4
|
* d'un contexte de requête — pour enrichir un événement d'audit avec la
|
|
@@ -20,5 +20,4 @@ function readAuditContext(context) {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
//#endregion
|
|
24
23
|
export { readAuditContext };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/audit/recordAudit.ts
|
|
2
2
|
/**
|
|
3
3
|
* Émet un événement d'audit **si** le service est présent — no-op sinon. La
|
|
4
4
|
* résolution se fait par le container (`auditService`) sur le **cold-path**
|
|
@@ -12,5 +12,4 @@
|
|
|
12
12
|
function recordAudit(container, event) {
|
|
13
13
|
(container?.get("auditService"))?.record(event);
|
|
14
14
|
}
|
|
15
|
-
//#endregion
|
|
16
15
|
export { recordAudit };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnonymousToken } from "../token/AnonymousToken.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/authenticator/AnonymousAuthenticator.ts
|
|
3
3
|
/**
|
|
4
4
|
* Acceptation EXPLICITE de l'anonymat dans une zone — le seul authenticator
|
|
5
5
|
* autorisé à produire un token non authentifié sans déclencher le Zero Trust.
|
|
@@ -32,5 +32,4 @@ var AnonymousAuthenticator = class {
|
|
|
32
32
|
return Promise.resolve();
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
//#endregion
|
|
36
35
|
export { AnonymousAuthenticator, AnonymousAuthenticator as default };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
+
import { isBoundToAccount } from "../accountBinding.js";
|
|
2
3
|
import { UserToken } from "../token/UserToken.js";
|
|
3
4
|
import { bearerToken } from "./bearer.js";
|
|
4
5
|
import { looksLikeApiKey, parseApiKey } from "../apikey/apiKeyFormat.js";
|
|
5
|
-
|
|
6
|
+
// nodefony/src/authenticator/ApiKeyAuthenticator.ts
|
|
6
7
|
const INVALID_TOKEN = "Invalid token";
|
|
7
8
|
/**
|
|
8
9
|
* Marqueur interne posé par `authenticate()` et consommé par `onSuccess()` :
|
|
@@ -81,6 +82,7 @@ var ApiKeyAuthenticator = class {
|
|
|
81
82
|
const invalidBefore = await store.getInvalidBefore(record.subjectId);
|
|
82
83
|
if (invalidBefore !== null && record.createdAt < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
|
|
83
84
|
const user = await this.#resolveUserOrReject(record.subjectId);
|
|
85
|
+
if (!isBoundToAccount(user, record.metadata["accountId"], record.metadata["credentialsVersion"])) throw new AuthenticationError(INVALID_TOKEN);
|
|
84
86
|
const last = record.lastUsedAt;
|
|
85
87
|
if (this.#throttleMs === 0 || last === null || now - last >= this.#throttleMs) token.setAttribute(MARK_USED_AT, now);
|
|
86
88
|
const ut = token;
|
|
@@ -160,5 +162,4 @@ var ApiKeyAuthenticator = class {
|
|
|
160
162
|
return this.#userProvider;
|
|
161
163
|
}
|
|
162
164
|
};
|
|
163
|
-
//#endregion
|
|
164
165
|
export { ApiKeyAuthenticator, ApiKeyAuthenticator as default };
|