@nodefony/security 10.0.0-alpha.6 → 10.0.0-alpha.8
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.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/config/config.js +2 -1
- package/dist/nodefony/src/SecuredArea.js +7 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +23 -9
- package/dist/types/nodefony/config/config.d.ts +2 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +15 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +6 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +22 -8
- package/docs/audit.md +1 -2
- package/docs/authorization.md +116 -11
- package/docs/firewall.md +51 -0
- package/docs/oauth2.md +9 -2
- package/docs/totp.md +1 -1
- package/package.json +13 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -77,8 +77,8 @@ import SecurityUserPassword from "./nodefony/command/security-user-password.js";
|
|
|
77
77
|
import SecurityToken from "./nodefony/command/security-token.js";
|
|
78
78
|
import { createSecurityAdminApi, parseAuditQuery, registerSecurityAdminApi } from "./nodefony/src/admin/SecurityAdminApi.js";
|
|
79
79
|
import { registerUserRevocationCascade } from "./nodefony/src/admin/userRevocationCascade.js";
|
|
80
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
81
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
80
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
81
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
82
82
|
import { tokenStatusCriteria } from "./nodefony/src/token/tokenCriteria.js";
|
|
83
83
|
import { AccessDeniedError } from "./nodefony/errors/AccessDeniedError.js";
|
|
84
84
|
import "./nodefony/errors/index.js";
|
|
@@ -44,6 +44,7 @@ const areaSchema = z.strictObject({
|
|
|
44
44
|
stateless: z.boolean().default(false).describe("Stratégie d'identité AU-DESSUS du protocole (HTTP reste stateless par nature). false (défaut) : la zone PEUT tenir un registre serveur — la session n'est créée qu'AU LOGIN (jamais pour un anonyme, zéro alloc), cookie opaque révocable (BFF). true : aucun registre — chaque requête porte sa preuve complète (JWT/clé API), la session est ignorée même si un cookie est présent. ⚠️ Une zone destinée à un appelant NON-NAVIGATEUR (service partenaire, script, agent) doit être `true` : laissée à `false`, elle marche à l'essai — un navigateur ou un `curl -c` renvoie le cookie posé — puis échoue chez le vrai client, qui ne stocke rien et repart anonyme à chaque requête. Le symptôme est un 401 intermittent en production, que rien dans le code ne montre."),
|
|
45
45
|
mode: z.enum(["first", "all"]).default("first").describe("Chaîne d'authenticators : 'first' = le premier qui reconnaît la requête authentifie (ex. cookie OU bearer) ; 'all' = tous doivent passer (ex. mtls+jwt zone admin)."),
|
|
46
46
|
authenticators: z.array(z.string()).default([]).describe("Authenticators de la zone (sémantique selon `mode`). Validés au boot contre le registre."),
|
|
47
|
+
roles: z.array(z.string()).default([]).describe("Rôle(s) exigés PAR DÉFAUT dans la zone, en OU (l'un suffit ; hiérarchie comprise). Vide (défaut) = la zone n'exige qu'une identité, et toute route qu'elle couvre est ouverte à n'importe quel compte authentifié — c'est un défaut OUVERT, qu'une surface d'administration ne doit pas garder. Renseigné, le rôle s'applique à toute route de la zone qui ne décide PAS elle-même de son autorisation : une route gardée (`@IsGranted`, `@RequireScope`) et le pont du plan d'administration (qui résout un rôle par point d'entrée) gardent la main — sans quoi une zone fermée à l'administrateur écraserait un point d'entrée voulu accessible à son propriétaire (`me`, `sessions/mine`). Un refus ici est un 403 (authentifié, pas autorisé), jamais un 401."),
|
|
47
48
|
entryPoint: z.string().optional().describe("Route de login/redirect si non authentifié."),
|
|
48
49
|
host: z.string().optional().describe("Domaine/vhost de la zone (ex. admin.exemple.com). Omis = tous domaines."),
|
|
49
50
|
resource: z.string().optional().describe("URI canonique de la ressource protégée — l'AUDIENCE qu'un jeton doit porter pour être accepté ici (RFC 8707 §2). Obligatoire dès que la zone liste un authenticator de jetons TIERS (`external-jwt`), qui refuse de démarrer sans elle. C'est ce qui empêche un jeton valide, délivré au même porteur pour un AUTRE service, d'être rejoué sur celui-ci. Elle s'ÉCRIT et ne se dérive pas du `Host` : derrière un relais, ce que le processus croit être son adresse n'est pas ce que le client a demandé, et l'audience doit être exactement celle que le serveur d'autorisation a inscrite dans le jeton. Ex. « https://api.example.com/mcp »."),
|
|
@@ -336,7 +337,7 @@ const securityConfigSchema = z.strictObject({
|
|
|
336
337
|
encoders: z.record(z.string(), encoderSchema).default(() => ({ default: encoderSchema.parse({ type: "argon2id" }) })).describe("Chaîne d'encodeurs de mot de passe. 1re entrée = principal (hash à la création), suivantes = legacy en lecture seule (migration au login). Défaut : un argon2id sûr — le firewall en dérive le service `passwordEncoder`."),
|
|
337
338
|
roleHierarchy: z.record(z.string(), z.array(z.string())).default({}).describe("Hiérarchie de rôles (ROLE_ADMIN: ['ROLE_USER'])."),
|
|
338
339
|
areas: z.record(z.string(), areaSchema).default({}).describe("Zones sécurisées (firewall) par nom."),
|
|
339
|
-
realtimeChannels: z.array(realtimeChannelRuleSchema).default([]).describe("Politiques d'autorisation par préfixe sur le temps réel. Couvre les canaux WS (subscribe/inbound) **ET les actions RPC** : le nom de méthode d'une frame entrante est soumis aux mêmes règles de préfixe — une règle `admin:` garde donc aussi l'action `admin:purge`. Étend/surcharge les défauts système (syslog:/orm:/node:/dashboard:/debugbar:/realtime:/cluster: + :health/:stats =
|
|
340
|
+
realtimeChannels: z.array(realtimeChannelRuleSchema).default([]).describe("Politiques d'autorisation par préfixe sur le temps réel. Couvre les canaux WS (subscribe/inbound) **ET les actions RPC** : le nom de méthode d'une frame entrante est soumis aux mêmes règles de préfixe — une règle `admin:` garde donc aussi l'action `admin:purge`. Étend/surcharge les défauts système (syslog:/orm:/node:/dashboard:/debugbar:/realtime:/cluster: + :health/:stats = ROLE_NODEFONY_ADMIN, l'échelle PLATEFORME : ces canaux exposent l'état du processus, pas celui d'une application). Ce qui n'est couvert par aucune règle reste libre, sauf gating `@RealtimeChannel` côté controller."),
|
|
340
341
|
cors: corsSchema.default(() => corsSchema.parse({})),
|
|
341
342
|
csrf: csrfSchema.default(() => csrfSchema.parse({})),
|
|
342
343
|
headers: headersSchema.default(() => headersSchema.parse({})),
|
|
@@ -16,6 +16,12 @@ var SecuredArea = class {
|
|
|
16
16
|
host;
|
|
17
17
|
realtime;
|
|
18
18
|
resource;
|
|
19
|
+
/**
|
|
20
|
+
* Rôles exigés par défaut dans la zone, ou `null` quand elle n'en exige
|
|
21
|
+
* aucun. `null` plutôt qu'un tableau vide : le hot path teste une référence,
|
|
22
|
+
* et aucune zone ne porte de tableau qu'elle n'utilise pas.
|
|
23
|
+
*/
|
|
24
|
+
roles;
|
|
19
25
|
constructor(name, config) {
|
|
20
26
|
this.name = name;
|
|
21
27
|
this.pattern = new RegExp(config.pattern, "u");
|
|
@@ -26,6 +32,7 @@ var SecuredArea = class {
|
|
|
26
32
|
this.host = config.host;
|
|
27
33
|
this.realtime = config.realtime;
|
|
28
34
|
this.resource = config.resource;
|
|
35
|
+
this.roles = config.roles.length > 0 ? config.roles : null;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Cœur du match — pathname (+ host éventuel) déjà extraits, SANS `context`.
|
|
@@ -2,13 +2,26 @@ import { NODEFONY_CHANNEL_NAMESPACE, PLATFORM_CHANNELS, PLATFORM_INBOUND, starts
|
|
|
2
2
|
//#region nodefony/src/realtime/frameAuthorizer.ts
|
|
3
3
|
/**
|
|
4
4
|
* Politique système par défaut des canaux d'**introspection serveur** : réservés
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* à l'exploitant de l'INSTANCE, pas à l'administrateur d'une application.
|
|
6
|
+
*
|
|
7
|
+
* Ce que ces canaux exposent est l'état du **processus** — journaux, base,
|
|
8
|
+
* métriques, supervision, et jusqu'au déclenchement d'un ramasse-miettes
|
|
9
|
+
* bloquant. C'est l'échelle PLATEFORME, celle que le préfixe `ROLE_NODEFONY_*`
|
|
10
|
+
* désigne : le seul rôle qui traverse l'isolation entre organisations
|
|
11
|
+
* clientes. Un `ROLE_ADMIN` est, lui, destiné à être scopé à une organisation ;
|
|
12
|
+
* le jour où plusieurs partagent une instance, il aurait lu la supervision du
|
|
13
|
+
* processus qui les héberge toutes.
|
|
14
|
+
*
|
|
15
|
+
* Le canal d'audit ({@link SECURITY_CHANNEL_POLICY}) partage désormais cette
|
|
16
|
+
* échelle — il ne se distingue plus par le rôle, mais par ce qu'il contient.
|
|
17
|
+
*
|
|
18
|
+
* Surchargeable finement par `realtimeChannels` : une application qui veut
|
|
19
|
+
* ouvrir un de ces canaux à un rôle d'organisation le DÉCLARE, elle ne l'hérite
|
|
20
|
+
* pas.
|
|
8
21
|
*/
|
|
9
22
|
const SYSTEM_CHANNEL_POLICY = {
|
|
10
23
|
authenticated: true,
|
|
11
|
-
roles: ["
|
|
24
|
+
roles: ["ROLE_NODEFONY_ADMIN"]
|
|
12
25
|
};
|
|
13
26
|
/**
|
|
14
27
|
* Namespace d'introspection serveur (observabilité) — s'y abonner expose l'état
|
|
@@ -24,11 +37,12 @@ const SYSTEM_CHANNEL_POLICY = {
|
|
|
24
37
|
*/
|
|
25
38
|
const DEFAULT_SYSTEM_PREFIXES = [NODEFONY_CHANNEL_NAMESPACE];
|
|
26
39
|
/**
|
|
27
|
-
* Plancher des canaux de **sécurité** (`nodefony:audit
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* Plancher des canaux de **sécurité** (`nodefony:audit`) : réservé à l'exploitant
|
|
41
|
+
* de l'instance (`ROLE_NODEFONY_ADMIN`). Même échelle que
|
|
42
|
+
* {@link SYSTEM_CHANNEL_POLICY} — le journal d'audit du pod n'est pas plus
|
|
43
|
+
* exposé que sa supervision, et aucun des deux ne se lit avec un rôle
|
|
44
|
+
* d'organisation. Cohérent avec le plan d'administration HTTP de l'audit
|
|
45
|
+
* (`SecurityAdminApi`, même rôle).
|
|
32
46
|
*
|
|
33
47
|
* Multi-tenant (futur) : `nodefony:audit` reste un canal **plateforme** (pod),
|
|
34
48
|
* jamais exposé à un user tenant ; l'événement portera le `tenantId` (via l'ALS)
|
|
@@ -8,6 +8,7 @@ declare const areaSchema: z.ZodObject<{
|
|
|
8
8
|
first: "first";
|
|
9
9
|
}>>;
|
|
10
10
|
authenticators: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
|
+
roles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
12
|
entryPoint: z.ZodOptional<z.ZodString>;
|
|
12
13
|
host: z.ZodOptional<z.ZodString>;
|
|
13
14
|
resource: z.ZodOptional<z.ZodString>;
|
|
@@ -34,6 +35,7 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
34
35
|
first: "first";
|
|
35
36
|
}>>;
|
|
36
37
|
authenticators: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
38
|
+
roles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
37
39
|
entryPoint: z.ZodOptional<z.ZodString>;
|
|
38
40
|
host: z.ZodOptional<z.ZodString>;
|
|
39
41
|
resource: z.ZodOptional<z.ZodString>;
|
|
@@ -52,6 +52,21 @@ export interface ISecuredArea {
|
|
|
52
52
|
* verrou WS consulte la même zone que HTTP (invariant `api.request` ≤ `GET`).
|
|
53
53
|
*/
|
|
54
54
|
readonly realtime: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Rôles exigés PAR DÉFAUT dans la zone — en OU, hiérarchie comprise —, ou
|
|
57
|
+
* `null` quand la zone n'exige qu'une identité.
|
|
58
|
+
*
|
|
59
|
+
* C'est le **filet** d'une surface d'administration : une route que personne
|
|
60
|
+
* n'a pensé à garder hérite du rôle de sa zone, au lieu d'être ouverte à
|
|
61
|
+
* n'importe quel compte authentifié. Une route qui décide elle-même de son
|
|
62
|
+
* autorisation — garde déclarée, ou pont du plan d'administration qui résout
|
|
63
|
+
* un rôle par point d'entrée — n'en dépend pas : c'est ce qui laisse exister
|
|
64
|
+
* un point d'entrée ouvert à son propriétaire (`me`, `sessions/mine`) dans
|
|
65
|
+
* une zone par ailleurs fermée.
|
|
66
|
+
*
|
|
67
|
+
* Un refus est un **403** (authentifié mais pas autorisé), jamais un 401.
|
|
68
|
+
*/
|
|
69
|
+
readonly roles: readonly string[] | null;
|
|
55
70
|
/** La requête tombe-t-elle dans cette zone ? (pattern + host éventuel). */
|
|
56
71
|
match(context: ContextType): boolean;
|
|
57
72
|
}
|
|
@@ -18,6 +18,12 @@ export declare class SecuredArea implements ISecuredArea {
|
|
|
18
18
|
readonly host?: string;
|
|
19
19
|
readonly realtime: boolean;
|
|
20
20
|
readonly resource?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Rôles exigés par défaut dans la zone, ou `null` quand elle n'en exige
|
|
23
|
+
* aucun. `null` plutôt qu'un tableau vide : le hot path teste une référence,
|
|
24
|
+
* et aucune zone ne porte de tableau qu'elle n'utilise pas.
|
|
25
|
+
*/
|
|
26
|
+
readonly roles: readonly string[] | null;
|
|
21
27
|
constructor(name: string, config: ISecurityAreaConfig);
|
|
22
28
|
/**
|
|
23
29
|
* Cœur du match — pathname (+ host éventuel) déjà extraits, SANS `context`.
|
|
@@ -48,9 +48,22 @@ export interface ISystemChannelRule {
|
|
|
48
48
|
export type FrameDenyReporter = (surface: "api.request" | "channel", target: string, reason: string, token: IRealtimeToken) => void;
|
|
49
49
|
/**
|
|
50
50
|
* Politique système par défaut des canaux d'**introspection serveur** : réservés
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* à l'exploitant de l'INSTANCE, pas à l'administrateur d'une application.
|
|
52
|
+
*
|
|
53
|
+
* Ce que ces canaux exposent est l'état du **processus** — journaux, base,
|
|
54
|
+
* métriques, supervision, et jusqu'au déclenchement d'un ramasse-miettes
|
|
55
|
+
* bloquant. C'est l'échelle PLATEFORME, celle que le préfixe `ROLE_NODEFONY_*`
|
|
56
|
+
* désigne : le seul rôle qui traverse l'isolation entre organisations
|
|
57
|
+
* clientes. Un `ROLE_ADMIN` est, lui, destiné à être scopé à une organisation ;
|
|
58
|
+
* le jour où plusieurs partagent une instance, il aurait lu la supervision du
|
|
59
|
+
* processus qui les héberge toutes.
|
|
60
|
+
*
|
|
61
|
+
* Le canal d'audit ({@link SECURITY_CHANNEL_POLICY}) partage désormais cette
|
|
62
|
+
* échelle — il ne se distingue plus par le rôle, mais par ce qu'il contient.
|
|
63
|
+
*
|
|
64
|
+
* Surchargeable finement par `realtimeChannels` : une application qui veut
|
|
65
|
+
* ouvrir un de ces canaux à un rôle d'organisation le DÉCLARE, elle ne l'hérite
|
|
66
|
+
* pas.
|
|
54
67
|
*/
|
|
55
68
|
export declare const SYSTEM_CHANNEL_POLICY: IChannelPolicy;
|
|
56
69
|
/**
|
|
@@ -67,11 +80,12 @@ export declare const SYSTEM_CHANNEL_POLICY: IChannelPolicy;
|
|
|
67
80
|
*/
|
|
68
81
|
export declare const DEFAULT_SYSTEM_PREFIXES: readonly ["nodefony:"];
|
|
69
82
|
/**
|
|
70
|
-
* Plancher des canaux de **sécurité** (`nodefony:audit
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
83
|
+
* Plancher des canaux de **sécurité** (`nodefony:audit`) : réservé à l'exploitant
|
|
84
|
+
* de l'instance (`ROLE_NODEFONY_ADMIN`). Même échelle que
|
|
85
|
+
* {@link SYSTEM_CHANNEL_POLICY} — le journal d'audit du pod n'est pas plus
|
|
86
|
+
* exposé que sa supervision, et aucun des deux ne se lit avec un rôle
|
|
87
|
+
* d'organisation. Cohérent avec le plan d'administration HTTP de l'audit
|
|
88
|
+
* (`SecurityAdminApi`, même rôle).
|
|
75
89
|
*
|
|
76
90
|
* Multi-tenant (futur) : `nodefony:audit` reste un canal **plateforme** (pod),
|
|
77
91
|
* jamais exposé à un user tenant ; l'événement portera le `tenantId` (via l'ALS)
|
package/docs/audit.md
CHANGED
|
@@ -584,8 +584,7 @@ Ces deux absences n'ont pas le même statut. Redis n'en aura pas, et la raison n
|
|
|
584
584
|
que le journal d'audit **croît sans borne**, se conserve des mois pour la conformité, et se
|
|
585
585
|
**consulte** (recherche, filtres, pagination) : garder tout ça en mémoire vive coûte cher pour un
|
|
586
586
|
motif d'accès qui n'est pas le sien. Mongo, lui, est un chemin **durable** :
|
|
587
|
-
l'audit y **manque**, et c'est un manque à combler (objectif « full NoSQL
|
|
588
|
-
P7.11) — un utilisateur choisit sa base de données, pas de perdre sa traçabilité.
|
|
587
|
+
l'audit y **manque**, et c'est un manque à combler (objectif « full NoSQL ») — un utilisateur choisit sa base de données, pas de perdre sa traçabilité.
|
|
589
588
|
|
|
590
589
|
En attendant, une application MongoDB qui veut un journal durable a trois voies : brancher un store
|
|
591
590
|
maison (voir l'extension ci-dessous), charger `@nodefony/drizzle` à côté de Mongo — même en SQLite
|
package/docs/authorization.md
CHANGED
|
@@ -87,6 +87,35 @@ Nodefony **centralise** la décision dans un service unique, appelé par les dé
|
|
|
87
87
|
(`@IsGranted`, `@RequireScope`) sur tous les transports, avec une posture **fail-closed** : au
|
|
88
88
|
moindre doute (voter qui plante, silence du jury, moteur absent), c'est refusé — jamais accordé.
|
|
89
89
|
|
|
90
|
+
## Où se situe Nodefony parmi les modèles de contrôle d'accès
|
|
91
|
+
|
|
92
|
+
Le contrôle d'accès a plusieurs façons de répondre à « as-tu le droit ? », selon **ce sur quoi**
|
|
93
|
+
la décision porte : un rôle porté par le compte (RBAC), des attributs du sujet, de la ressource et
|
|
94
|
+
du contexte (ABAC), une relation dans un graphe (ReBAC), ou une politique écrite hors du code
|
|
95
|
+
(XACML, et les moteurs qui s'en inspirent). Ce ne sont pas des concurrents : les systèmes réels en
|
|
96
|
+
empilent plusieurs.
|
|
97
|
+
|
|
98
|
+
Nodefony fait du **RBAC hiérarchique** — des rôles, et une hiérarchie qui dit lesquels en couvrent
|
|
99
|
+
d'autres, au sens des composantes _Core_ et _Hierarchical_ du modèle NIST (ANSI INCITS 359-2004,
|
|
100
|
+
révisée 2012). Le reste se **branche** au jury, décrit plus bas : un voter maison qui lit des
|
|
101
|
+
attributs fait de l'ABAC, un voter qui interroge un moteur de graphe (OpenFGA, SpiceDB, Ory Keto —
|
|
102
|
+
famille issue de l'article _Zanzibar_, Google, 2019) fait du ReBAC.
|
|
103
|
+
|
|
104
|
+
**Ce qui n'est PAS couvert**, et qu'il vaut mieux savoir avant de concevoir :
|
|
105
|
+
|
|
106
|
+
- **la séparation des devoirs** (deux rôles incompatibles sur un même compte) n'est pas outillée ;
|
|
107
|
+
elle se joue à l'affectation des rôles, pas à la décision ;
|
|
108
|
+
- **le filtrage d'une liste** — « montre-moi seulement les lignes que j'ai le droit de voir » — ne
|
|
109
|
+
passe pas par le jury : chaque décision porte sur **une** ressource. Ce filtrage appartient à ta
|
|
110
|
+
requête de dépôt ;
|
|
111
|
+
- **le contexte de la requête** (heure, adresse, en-têtes, corps) n'est pas au contrat d'un voter ;
|
|
112
|
+
ce qui lui parvient, c'est le jeton et le sujet ;
|
|
113
|
+
- **aucune décision « accordé, à condition que »** (les _obligations_ de XACML) : le verdict est
|
|
114
|
+
binaire.
|
|
115
|
+
|
|
116
|
+
En pratique, et c'est le cas dans 95 % des applications : des rôles, plus **un** voter d'ownership
|
|
117
|
+
pour les cas où le rôle ne suffit pas.
|
|
118
|
+
|
|
90
119
|
## La vision Nodefony — un jury découplé et fail-closed
|
|
91
120
|
|
|
92
121
|
`Authorization.decide(token, attribute, subject?)` (`authorization.ts:70`) itère les voters, teste
|
|
@@ -302,7 +331,7 @@ Dès le `DENY`, le jury **s'arrête** — court-circuit, inutile de finir (`auth
|
|
|
302
331
|
|
|
303
332
|
**Contre-exemple piégeux** : le veto ne traverse **pas** une clause OR. Dans
|
|
304
333
|
`@IsGranted(["ROLE_ADMIN", "doc.edit"])`, chaque attribut est un **jury séparé**
|
|
305
|
-
(`Resolver.ts:
|
|
334
|
+
(`Resolver.ts:689-702`) : si `ROLE_ADMIN` est accordé, `doc.edit` — et son veto — n'est même pas
|
|
306
335
|
consulté. Un interdit absolu se porte en clause **AND** : empiler `@IsGranted("ROLE_ADMIN")` puis
|
|
307
336
|
`@IsGranted("doc.edit", { subject: "id" })`.
|
|
308
337
|
|
|
@@ -327,16 +356,16 @@ se voit au premier test.
|
|
|
327
356
|
## 🧰 Déclarer l'exigence — `@IsGranted`, `@RequireScope`, `@Anonymous`
|
|
328
357
|
|
|
329
358
|
Les décorateurs n'écrivent **que des métadonnées** (0 import `@nodefony/security`, 0 cycle) ; le
|
|
330
|
-
moteur `authorization` est résolu **par nom** au runtime (`Resolver.ts:
|
|
359
|
+
moteur `authorization` est résolu **par nom** au runtime (`Resolver.ts:673-674`) :
|
|
331
360
|
|
|
332
361
|
| Déclaration | Sémantique |
|
|
333
362
|
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
334
|
-
| `@IsGranted("ROLE_ADMIN")` | un attribut — rôle, scope ou verbe métier (`IsGranted()`, `routerDecorators.ts:
|
|
363
|
+
| `@IsGranted("ROLE_ADMIN")` | un attribut — rôle, scope ou verbe métier (`IsGranted()`, `routerDecorators.ts:878`) |
|
|
335
364
|
| `@IsGranted(["A", "B"])` | **OR interne** — un attribut accordé suffit (`SecurityClause.anyOf`, `routerDecorators.ts:407-412`) |
|
|
336
365
|
| empiler `@IsGranted` / `@RequireScope` | **AND** — toutes les clauses doivent passer (`SecurityRequirement.clauses`, `routerDecorators.ts:426`) |
|
|
337
366
|
| décorateur de classe + de méthode | fusion en **AND**, figée UNE fois par route (`computeSecurityRequirement()`, `routerDecorators.ts:1500`) |
|
|
338
|
-
| `@IsGranted("doc.edit", { subject: "id" })` | le param de route `id` est passé au voter (`Resolver._resolveSubject()`, `Resolver.ts:
|
|
339
|
-
| `@RequireScope("orders:read")` | axe scope — metadata dédiée, fusionnée dans le même `SecurityRequirement` (`RequireScope()`, `routerDecorators.ts:
|
|
367
|
+
| `@IsGranted("doc.edit", { subject: "id" })` | le param de route `id` est passé au voter (`Resolver._resolveSubject()`, `Resolver.ts:743-747`) |
|
|
368
|
+
| `@RequireScope("orders:read")` | axe scope — metadata dédiée, fusionnée dans le même `SecurityRequirement` (`RequireScope()`, `routerDecorators.ts:975`) |
|
|
340
369
|
| `@Anonymous()` | action **publique** — override les gardes de classe (`security: null`) + skip l'authn (`Anonymous()`, `routerDecorators.ts:887`) |
|
|
341
370
|
| `@CurrentUser()` | injecte l'utilisateur de l'ALS — jamais le credential (`CurrentUser`, `routerDecorators.ts:1236`) |
|
|
342
371
|
|
|
@@ -344,7 +373,7 @@ La garde s'évalue dans `Resolver.executeAction()` **AVANT** l'instanciation DI
|
|
|
344
373
|
403 court-circuite tout, y compris `initialize()` (`_enforceSecurity`, `Resolver.ts:331-336`). Le
|
|
345
374
|
même `executeAction` sert le pipeline HTTP **et** l'invoke WS-RPC : une garde, tous les
|
|
346
375
|
transports. L'enforcement déroule chaque clause : OR interne via un `decide()` par attribut, AND
|
|
347
|
-
entre clauses (`Resolver._enforceSecurity()`, `Resolver.ts:
|
|
376
|
+
entre clauses (`Resolver._enforceSecurity()`, `Resolver.ts:672-702`).
|
|
348
377
|
|
|
349
378
|
> [!IMPORTANT]
|
|
350
379
|
> **Fail-closed intégral** : route gardée mais moteur `authorization` absent (module security non
|
|
@@ -373,6 +402,25 @@ Capte les attributs `ROLE_*` (`RoleVoter.supports()`, `RoleVoter.ts:25-27`) et v
|
|
|
373
402
|
(`RoleVoter.ts:30-32`), posée par le firewall au boot (`firewall.ts:206`).
|
|
374
403
|
- Sync par nature → `Promise.resolve`, pas de wrapper `async` inutile (`RoleVoter.ts:36-38`).
|
|
375
404
|
|
|
405
|
+
#### Deux échelles de rôles — plateforme et organisation
|
|
406
|
+
|
|
407
|
+
Une **convention de nom** sépare deux échelles, et c'est elle qui prépare le jour où plusieurs
|
|
408
|
+
organisations partageront une instance :
|
|
409
|
+
|
|
410
|
+
| Préfixe | Échelle | Qui le porte | Scopable ? |
|
|
411
|
+
| ----------------- | ---------------- | -------------------------- | ---------- |
|
|
412
|
+
| `ROLE_NODEFONY_*` | **plateforme** | l'exploitant de l'instance | **jamais** |
|
|
413
|
+
| `ROLE_*` | **organisation** | un compte de l'application | oui |
|
|
414
|
+
|
|
415
|
+
L'échelle plateforme est la seule qui traverse l'isolation entre organisations. Les ressources du
|
|
416
|
+
**processus** — journaux, base, supervision, ramasse-miettes — sont donc gardées par elle, jamais
|
|
417
|
+
par un rôle d'organisation : un administrateur applicatif n'a pas à lire l'état du serveur qui
|
|
418
|
+
héberge aussi les autres.
|
|
419
|
+
|
|
420
|
+
> ⚠️ **Ne donne jamais un `ROLE_NODEFONY_*` à un compte client.** Un « administrateur » chez le
|
|
421
|
+
> client, c'est `ROLE_ADMIN`. C'est une convention : **rien dans le code ne l'impose** pour tes
|
|
422
|
+
> propres rôles — seules les politiques de plateforme du framework sont vérifiées par un test.
|
|
423
|
+
|
|
376
424
|
### `scope` — l'axe « ce qu'une clé déléguée peut faire »
|
|
377
425
|
|
|
378
426
|
Frère du `role` sur l'autre axe. Capte la forme conventionnée `api:action` — un `:`, jamais
|
|
@@ -400,6 +448,49 @@ voir [firewall](./firewall.md)) et fait deux choses au boot :
|
|
|
400
448
|
le boot **jette avec le chemin complet** `A → B → A` (`RoleHierarchyWalker.ts:69-95`) : jamais
|
|
401
449
|
de boucle infinie silencieuse en production.
|
|
402
450
|
|
|
451
|
+
## 🛡️ Le rôle par défaut d'une zone — et ce qu'il n'écrase pas
|
|
452
|
+
|
|
453
|
+
Une zone du pare-feu peut exiger un rôle **par défaut** chez elle (`roles`, voir
|
|
454
|
+
[firewall](./firewall.md)). C'est le filet d'un espace d'administration : une route que personne
|
|
455
|
+
n'a pensé à garder hérite du rôle de sa zone, au lieu d'être ouverte à tout compte connecté.
|
|
456
|
+
|
|
457
|
+
La précédence est un **remplacement, jamais un cumul de rôles** : si l'action déclare sa propre
|
|
458
|
+
garde de rôle, c'est elle qui décide — y compris pour ouvrir plus largement que la zone. C'est
|
|
459
|
+
voulu : une page réservée à l'exploitant (`ROLE_SUPERVISOR`) doit rester atteignable dans une zone
|
|
460
|
+
fermée à l'administrateur de plateforme.
|
|
461
|
+
|
|
462
|
+
Quatre cas gardent la main, et chacun est une déclaration explicite :
|
|
463
|
+
|
|
464
|
+
| Ce qui décide à la place | Pourquoi |
|
|
465
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
466
|
+
| une garde de **rôle** (`@IsGranted`) | l'action a dit ce qu'elle exige |
|
|
467
|
+
| le **pont du plan d'administration** | il résout un rôle par point d'entrée, sous une route unique — d'où l'existence de `me`, `mine` |
|
|
468
|
+
| une route qui **est** le mécanisme d'auth | lui imposer un rôle serait un verrou dont la clé est à l'intérieur |
|
|
469
|
+
| l'**ouverture** d'une connexion WebSocket | une connexion n'accède à rien ; ce sont les frames qui accèdent, et chacune est gardée séparément |
|
|
470
|
+
|
|
471
|
+
> 🔴 **`@RequireScope` seul n'est pas une décision d'identité.** Un scope dit ce qu'une clé
|
|
472
|
+
> déléguée peut faire en ton nom ; sur une session humaine, le voter de scope accorde sans
|
|
473
|
+
> condition. Une action gardée par un scope seul reste donc soumise au rôle de sa zone — les deux
|
|
474
|
+
> axes se **cumulent**. Les traiter comme interchangeables ouvrait la route à tout compte connecté.
|
|
475
|
+
|
|
476
|
+
Le dernier cas mérite d'être compris, parce qu'il ressemble à un trou sans en être un : une frame
|
|
477
|
+
`api.request {path}` repasse par la **même** décision que la requête HTTP équivalente. L'invariant
|
|
478
|
+
tenu est `api.request {path}` ≤ `GET {path}`.
|
|
479
|
+
|
|
480
|
+
## 🚪 Trois portes décident — et deux ne passent pas par le jury
|
|
481
|
+
|
|
482
|
+
C'est la chose la plus utile à savoir avant d'écrire un voter, et la plus facile à ignorer :
|
|
483
|
+
|
|
484
|
+
| Porte | Qui tranche | Le jury est-il consulté ? |
|
|
485
|
+
| ------------------------------------------ | ----------------------------------- | ------------------------- |
|
|
486
|
+
| une action gardée (`@IsGranted`), la zone | `Authorization.decide()` | **oui** |
|
|
487
|
+
| un point d'entrée du plan d'administration | son rôle déclaré (`isAdminGranted`) | non |
|
|
488
|
+
| une frame WebSocket (canal, `api.request`) | la politique de canal | non |
|
|
489
|
+
|
|
490
|
+
Les trois appliquent la **hiérarchie** de rôles, donc un même rôle y rend le même verdict. Mais un
|
|
491
|
+
voter maison — ABAC, ReBAC, ownership — n'est consulté que par la première. Une règle métier qui
|
|
492
|
+
doit valoir partout se place donc dans le code de l'action, pas seulement dans un voter.
|
|
493
|
+
|
|
403
494
|
## 🧩 Étendre le jury — le contrat et le registre
|
|
404
495
|
|
|
405
496
|
Le contrat `IAccessVoter` (`IAccessVoter.ts:20-26`) tient en deux méthodes :
|
|
@@ -432,11 +523,20 @@ compilation** — rien à scanner au runtime ; le registre **est** le marqueur e
|
|
|
432
523
|
|
|
433
524
|
## 📜 Normes appliquées
|
|
434
525
|
|
|
435
|
-
| Domaine | Norme / posture
|
|
436
|
-
| -------------------------- |
|
|
437
|
-
| Contrôle d'accès | OWASP Top 10 **A01** (IDOR, élévation)
|
|
438
|
-
| Modèle
|
|
439
|
-
|
|
|
526
|
+
| Domaine | Norme / posture | Ancrage |
|
|
527
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
528
|
+
| Contrôle d'accès | OWASP Top 10 **A01** (IDOR, élévation) | défaut `DENY` du jury (`authorization.ts:100-108`) |
|
|
529
|
+
| Modèle de rôles | RBAC **hiérarchique**, au sens des composantes _Core_ et _Hierarchical_ d'ANSI INCITS 359 — sans séparation des devoirs | `RoleHierarchyWalker.reachableRoles()` |
|
|
530
|
+
| Combinaison des votes | _deny-overrides_ : un refus l'emporte, le silence ferme | `Authorization.decide()` |
|
|
531
|
+
| Scopes délégués | **RFC 6749 §3.3** — définis par le serveur d'autorisation ; sans effet sur une session humaine | `ScopeVoter.vote()` |
|
|
532
|
+
| Modèle | **Zero Trust** (fermé par défaut) | 403 fail-closed du Resolver (`Resolver.ts:582-584`) |
|
|
533
|
+
| Journalisation de sécurité | audit des refus, jamais des octrois | `#auditDeny` → `recordAudit` (`authorization.ts:113-142`) |
|
|
534
|
+
|
|
535
|
+
> Ce tableau dit ce que le code **fait**, pas ce qu'il revendique : Nodefony n'est pas « conforme
|
|
536
|
+
> ANSI 359 » (ni sessions de rôles, ni séparation des devoirs), pas « conforme XACML » (un seul
|
|
537
|
+
> algorithme de combinaison, aucune obligation), et pas « compatible Zanzibar » (pas d'expansion,
|
|
538
|
+
> pas de liste, pas de jeton de cohérence). Ce qu'il offre, c'est de quoi **brancher** ces moteurs
|
|
539
|
+
> pour une décision ponctuelle.
|
|
440
540
|
|
|
441
541
|
## ⚡ Performance & mémoire
|
|
442
542
|
|
|
@@ -452,6 +552,11 @@ compilation** — rien à scanner au runtime ; le registre **est** le marqueur e
|
|
|
452
552
|
|
|
453
553
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
454
554
|
|
|
555
|
+
| Symptôme | Cause | Correction |
|
|
556
|
+
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
557
|
+
| Une action gardée par `@RequireScope` seul reste accessible à un humain | le voter de scope accorde sans condition à une session — il ne contraint que les clés déléguées | ajouter un `@IsGranted`, ou compter sur le rôle par défaut de la zone |
|
|
558
|
+
| `@RequireScope("openid")` refuse tout, sans message | un scope sans `:` n'est capté par aucun voter → le jury ferme par défaut | employer la forme `api:action`, ou déclarer un voter pour cette famille |
|
|
559
|
+
|
|
455
560
|
| Symptôme | Cause (dans le code) | Correction |
|
|
456
561
|
| -------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
457
562
|
| Accès refusé alors que le rôle existe | Attribut mal formé (pas `ROLE_…`) → le `RoleVoter` n'entre pas | Respecter le préfixe `ROLE_` |
|
package/docs/firewall.md
CHANGED
|
@@ -431,6 +431,57 @@ muets (volume, pas un signal). Les voters sont instanciés **une fois au boot**
|
|
|
431
431
|
ou futur (`mtls`, `agent`…) — est traité comme scopable, donc **bridé par défaut**.
|
|
432
432
|
- En une ligne : rôles = qui tu es ; scopes = ce qu'une **clé** a le droit de faire.
|
|
433
433
|
|
|
434
|
+
### `roles` — le rôle exigé PAR DÉFAUT dans une zone
|
|
435
|
+
|
|
436
|
+
Une zone qui ne liste que des authenticators exige une **identité**, rien de plus : toute route
|
|
437
|
+
qu'elle couvre est alors ouverte à **n'importe quel compte connecté**. C'est acceptable pour une
|
|
438
|
+
application ; c'est un défaut ouvert pour une surface d'administration, où l'oubli d'une garde ne
|
|
439
|
+
devrait jamais se solder par une fuite.
|
|
440
|
+
|
|
441
|
+
`roles` renverse ce défaut. Il se déclare **une fois sur la zone**, au lieu d'être recopié sur
|
|
442
|
+
chaque route :
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
use("@nodefony/security", {
|
|
446
|
+
areas: {
|
|
447
|
+
admin: {
|
|
448
|
+
pattern: "^/back-office(/|$)",
|
|
449
|
+
authenticators: ["session"],
|
|
450
|
+
roles: ["ROLE_ADMIN"], // ← exigé par défaut dans toute la zone
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Les rôles listés sont en **OU** — un seul suffit — et la **hiérarchie s'applique** : un
|
|
457
|
+
`ROLE_NODEFONY_ADMIN` satisfait `ROLE_ADMIN` si la hiérarchie le dit. Un refus est un **403**
|
|
458
|
+
(authentifié, mais pas autorisé), jamais un 401. Omettre la clé laisse le comportement historique
|
|
459
|
+
inchangé : la zone n'exige qu'une identité.
|
|
460
|
+
|
|
461
|
+
#### Ce que le rôle de zone n'écrase PAS
|
|
462
|
+
|
|
463
|
+
C'est un **défaut**, pas une exclusivité. Quatre cas gardent la main, et chacun est une déclaration
|
|
464
|
+
explicite du code — jamais un contournement :
|
|
465
|
+
|
|
466
|
+
| Ce qui décide à la place | Pourquoi |
|
|
467
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
468
|
+
| Une **garde d'action** (`@IsGranted`, `@RequireScope`) | L'action a déjà dit ce qu'elle exige. Sans cela, une page réservée à l'exploitant deviendrait inatteignable pour lui. |
|
|
469
|
+
| Une route **`selfGuarded`** | Elle tranche son autorisation dans son code — typiquement parce qu'elle ne sert QUE le porteur courant. |
|
|
470
|
+
| Un **`bypassFirewall`** | La route EST le mécanisme d'authentification : lui imposer un rôle serait un verrou dont la clé est à l'intérieur. |
|
|
471
|
+
| L'**ouverture d'une connexion WebSocket** | Une connexion n'accède à rien ; ce sont les frames qui accèdent, et chacune est gardée pour son compte. |
|
|
472
|
+
|
|
473
|
+
Le dernier cas mérite d'être compris, parce qu'il pourrait passer pour un trou. Il n'en est pas un :
|
|
474
|
+
une frame `api.request {path}` **repasse par la même décision** que la requête HTTP équivalente.
|
|
475
|
+
L'invariant tenu est donc `api.request {path}` ≤ `GET {path}` — on n'obtient jamais par la socket ce
|
|
476
|
+
que la porte HTTP refuse. Ce que la dispense évite, c'est qu'un compte sans rôle d'administration ne
|
|
477
|
+
puisse plus ouvrir **aucune** socket, et perde du même coup le self-service dont il est le seul
|
|
478
|
+
destinataire.
|
|
479
|
+
|
|
480
|
+
> ⚠️ **`selfGuarded` n'est pas un interrupteur pour faire taire un refus.** Le poser sur une route
|
|
481
|
+
> qui ne décide de rien la rend accessible à tout compte connecté, en silence. Il se justifie par ce
|
|
482
|
+
> que fait le CODE de l'action : scoper au porteur courant, ou résoudre un rôle par point d'entrée.
|
|
483
|
+
> Dans le doute, une garde d'action explicite est toujours préférable.
|
|
484
|
+
|
|
434
485
|
### La hiérarchie de rôles
|
|
435
486
|
|
|
436
487
|
`RoleHierarchyWalker` (`src/RoleHierarchyWalker.ts`) : `ROLE_ADMIN` hérite `ROLE_USER`, etc.
|
package/docs/oauth2.md
CHANGED
|
@@ -163,6 +163,13 @@ reste de l'application démarre, le bouton correspondant n'apparaît simplement
|
|
|
163
163
|
Un fournisseur n'est monté **que si ses deux secrets sont présents** : pas de bouton mort sur l'écran
|
|
164
164
|
de login quand la variable manque.
|
|
165
165
|
|
|
166
|
+
> **Pourquoi ces deux noms n'ont pas de préfixe `NF_`, contrairement à la règle.** Le préfixe dit à
|
|
167
|
+
> qui appartient la valeur, pas qui la lit — et `GITHUB_CLIENT_ID` vous est **délivré par GitHub**.
|
|
168
|
+
> Son nom est celui que la documentation du fournisseur, Auth.js et Passport emploient déjà : si
|
|
169
|
+
> votre application en a une, Nodefony la lit telle quelle plutôt que de vous faire recopier la même
|
|
170
|
+
> valeur sous un second nom, qui finirait par diverger. La base des callbacks, elle, n'est émise par
|
|
171
|
+
> personne : c'est un réglage que votre application se donne, donc `NF_OAUTH_REDIRECT_BASE`.
|
|
172
|
+
|
|
166
173
|
```typescript
|
|
167
174
|
// env.ts — SEUL lecteur de process.env (catalogue typé, validé au boot).
|
|
168
175
|
// nodefony.config.ts — `ctx.env` EST ce catalogue (typé par le paramètre générique).
|
|
@@ -173,7 +180,7 @@ export const env = defineEnv({
|
|
|
173
180
|
GITHUB_CLIENT_SECRET: envString({ optional: true }),
|
|
174
181
|
// Base des callbacks : doit correspondre EXACTEMENT à l'URL enregistrée chez
|
|
175
182
|
// le fournisseur (RFC 9700 — comparaison de chaînes, pas de préfixe).
|
|
176
|
-
|
|
183
|
+
NF_OAUTH_REDIRECT_BASE: envString({ default: "https://localhost:5152" }),
|
|
177
184
|
});
|
|
178
185
|
|
|
179
186
|
export default defineConfig<typeof env>((ctx) => ({
|
|
@@ -194,7 +201,7 @@ export default defineConfig<typeof env>((ctx) => ({
|
|
|
194
201
|
github: {
|
|
195
202
|
clientId: ctx.env.GITHUB_CLIENT_ID,
|
|
196
203
|
clientSecret: ctx.env.GITHUB_CLIENT_SECRET,
|
|
197
|
-
redirectUri: `${ctx.env.
|
|
204
|
+
redirectUri: `${ctx.env.NF_OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/github/callback`,
|
|
198
205
|
},
|
|
199
206
|
}
|
|
200
207
|
: {}),
|
package/docs/totp.md
CHANGED
|
@@ -577,7 +577,7 @@ Deux pièges de lecture, signalés dans l'entité elle-même :
|
|
|
577
577
|
| `mongoose` | — | — | ⏳ manquant, à venir |
|
|
578
578
|
| `redis` | — | — | ⏳ manquant, à venir |
|
|
579
579
|
|
|
580
|
-
Ces deux absences sont des **manques**, pas des choix de périmètre
|
|
580
|
+
Ces deux absences sont des **manques**, pas des choix de périmètre —
|
|
581
581
|
mais elles se comblent à deux régimes différents.
|
|
582
582
|
|
|
583
583
|
`redis` le portera **en opt-in explicite, jamais choisi par `auto`** — exactement le régime des
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/security",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.8",
|
|
4
4
|
"description": "Pare-feu applicatif par zones pour Nodefony : authentification, autorisation par rôles, protection CSRF, journal d'audit",
|
|
5
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -47,26 +47,26 @@
|
|
|
47
47
|
"@simplewebauthn/server": "14.0.2",
|
|
48
48
|
"jose": "6.2.12",
|
|
49
49
|
"tslib": "2.8.1",
|
|
50
|
-
"zod": "^4.6.
|
|
50
|
+
"zod": "^4.6.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
54
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
55
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
56
|
-
"@types/node": "26.
|
|
57
|
-
"@vitest/coverage-v8": "5.0.
|
|
58
|
-
"nodefony": "^10.0.0-alpha.
|
|
53
|
+
"@nodefony/framework": "^10.0.0-alpha.8",
|
|
54
|
+
"@nodefony/http": "^10.0.0-alpha.8",
|
|
55
|
+
"@nodefony/user": "^10.0.0-alpha.8",
|
|
56
|
+
"@types/node": "26.6.2",
|
|
57
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
58
|
+
"nodefony": "^10.0.0-alpha.8",
|
|
59
59
|
"rimraf": "6.1.3",
|
|
60
|
-
"vitest": "5.0.
|
|
60
|
+
"vitest": "5.0.1"
|
|
61
61
|
},
|
|
62
62
|
"license": "Apache-2.0",
|
|
63
63
|
"readmeFilename": "README.md",
|
|
64
64
|
"contributors": [],
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
67
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
68
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
69
|
-
"nodefony": "^10.0.0-alpha.
|
|
66
|
+
"@nodefony/framework": "^10.0.0-alpha.8",
|
|
67
|
+
"@nodefony/http": "^10.0.0-alpha.8",
|
|
68
|
+
"@nodefony/user": "^10.0.0-alpha.8",
|
|
69
|
+
"nodefony": "^10.0.0-alpha.8"
|
|
70
70
|
},
|
|
71
71
|
"files": [
|
|
72
72
|
"dist",
|