@nodefony/security 10.0.0-beta.4 → 10.0.0-beta.5
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.
|
@@ -254,7 +254,7 @@ function authorizeChannel(channel, token, firewall, resolver, systemRules, onDen
|
|
|
254
254
|
* (api.request) ou la politique du canal (subscribe/inbound).
|
|
255
255
|
*
|
|
256
256
|
* Trois surfaces gardées :
|
|
257
|
-
* - `api.request {path}` (pont
|
|
257
|
+
* - `api.request {path}` (pont socket) : re-match de zone HTTP → zone
|
|
258
258
|
* protégée + anonyme = refus (autorisation de ZONE, identique à `GET {path}` ;
|
|
259
259
|
* la méthode/action est gardée en aval par le router + `@IsGranted`).
|
|
260
260
|
* - `subscribe {channel}` : politique de canal (système plancher + déclaration
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
|
|
2
|
+
import { DEFAULT_SYSTEM_RULES, buildFrameAuthorizer } from "../src/realtime/frameAuthorizer.js";
|
|
3
|
+
// nodefony/testing/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@nodefony/security/testing` — **identité d'essai et VRAI verrou de frame**,
|
|
6
|
+
* pour éprouver un canal ou une action temps réel protégés.
|
|
7
|
+
*
|
|
8
|
+
* Le harnais de `@nodefony/realtime/testing` accepte une identité et un verrou,
|
|
9
|
+
* mais n'en fournit aucun : `@nodefony/realtime` ne connaît pas la sécurité.
|
|
10
|
+
* Faute de mieux, une application écrivait un jeton factice et un verrou qui ne
|
|
11
|
+
* regardait que `isAuthenticated()` — son test passait, et n'éprouvait PAS la
|
|
12
|
+
* politique de rôles du canal. Un canal mal protégé y était vert. Deux agents
|
|
13
|
+
* sur deux l'ont fait, en recopiant le jeton que montrait le test généré.
|
|
14
|
+
*
|
|
15
|
+
* Ce module rend les deux pièces, et le verrou rendu est celui du pare-feu —
|
|
16
|
+
* {@link buildFrameAuthorizer}, la même fabrique que le boot branche sur le hub,
|
|
17
|
+
* avec la même hiérarchie de rôles et les mêmes canaux système. Seuls manquent
|
|
18
|
+
* le journal d'audit (un refus en test n'est pas une transition de sécurité) et
|
|
19
|
+
* les ZONES HTTP : une frame `api.request` y passe comme hors de toute zone, et
|
|
20
|
+
* la garde d'une route s'éprouve par HTTP, pas ici.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { getRealtimeHub } from "@nodefony/realtime";
|
|
24
|
+
* import { createRealtimeHarness } from "@nodefony/realtime/testing";
|
|
25
|
+
* import {
|
|
26
|
+
* createTestFrameAuthorizer,
|
|
27
|
+
* createTestIdentity,
|
|
28
|
+
* } from "@nodefony/security/testing";
|
|
29
|
+
*
|
|
30
|
+
* const h = createRealtimeHarness((ctx) => new ChatController(ctx), {
|
|
31
|
+
* identity: createTestIdentity({ identifier: "alice", roles: ["ROLE_USER"] }),
|
|
32
|
+
* frameAuthorizer: createTestFrameAuthorizer(getRealtimeHub()),
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Une identité de connexion AUTHENTIFIÉE, telle que le pare-feu l'aurait posée
|
|
40
|
+
* au handshake.
|
|
41
|
+
*
|
|
42
|
+
* Pour éprouver un visiteur anonyme, ne rien passer au harnais : il pose alors
|
|
43
|
+
* le jeton anonyme, comme le repli Zero Trust du runtime.
|
|
44
|
+
*
|
|
45
|
+
* @param options - identifiant, rôles, scopes, utilisateur.
|
|
46
|
+
* @returns un jeton à passer en `identity` au harnais temps réel.
|
|
47
|
+
*/
|
|
48
|
+
function createTestIdentity(options = {}) {
|
|
49
|
+
const identifier = options.identifier ?? "alice";
|
|
50
|
+
const roles = options.roles ?? ["ROLE_USER"];
|
|
51
|
+
return new TestToken(identifier, roles, options.scopes ?? [], options.user ?? {
|
|
52
|
+
identifier,
|
|
53
|
+
roles: [...roles]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Le jeton que rend {@link createTestIdentity}. */
|
|
57
|
+
var TestToken = class {
|
|
58
|
+
identifier;
|
|
59
|
+
roles;
|
|
60
|
+
scopes;
|
|
61
|
+
user;
|
|
62
|
+
type = "session";
|
|
63
|
+
constructor(identifier, roles, scopes, user) {
|
|
64
|
+
this.identifier = identifier;
|
|
65
|
+
this.roles = roles;
|
|
66
|
+
this.scopes = scopes;
|
|
67
|
+
this.user = user;
|
|
68
|
+
}
|
|
69
|
+
getUserIdentifier() {
|
|
70
|
+
return this.identifier;
|
|
71
|
+
}
|
|
72
|
+
isAuthenticated() {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
getRoles() {
|
|
76
|
+
return [...this.roles];
|
|
77
|
+
}
|
|
78
|
+
getScopes() {
|
|
79
|
+
return [...this.scopes];
|
|
80
|
+
}
|
|
81
|
+
getAttribute(key) {
|
|
82
|
+
return key === "user" ? this.user : void 0;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Le VRAI verrou de frame du pare-feu, pour le harnais temps réel.
|
|
87
|
+
*
|
|
88
|
+
* C'est lui qui applique `@RealtimeChannel(nom, { roles })`,
|
|
89
|
+
* `@RealtimeAction(nom, { roles })` et les canaux système : sans lui, une
|
|
90
|
+
* politique déclarée n'est appliquée par personne — en test comme au runtime.
|
|
91
|
+
*
|
|
92
|
+
* @param channels - ce qui connaît les politiques déclarées : le hub temps réel
|
|
93
|
+
* (`getRealtimeHub()`), lu à CHAQUE frame — les politiques que le controller
|
|
94
|
+
* enregistre à sa construction y sont donc vues.
|
|
95
|
+
* @param options - hiérarchie de rôles de l'application.
|
|
96
|
+
* @returns le verrou à passer en `frameAuthorizer` au harnais.
|
|
97
|
+
*/
|
|
98
|
+
function createTestFrameAuthorizer(channels, options = {}) {
|
|
99
|
+
const walker = new RoleHierarchyWalker(options.roleHierarchy ?? {});
|
|
100
|
+
return buildFrameAuthorizer({
|
|
101
|
+
matchPath: () => null,
|
|
102
|
+
hasRole: (userRoles, required) => walker.hasRole(userRoles, required)
|
|
103
|
+
}, {
|
|
104
|
+
channelResolver: channels,
|
|
105
|
+
systemRules: DEFAULT_SYSTEM_RULES
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export { createTestFrameAuthorizer, createTestIdentity };
|
|
@@ -39,7 +39,7 @@ export interface ISystemChannelRule {
|
|
|
39
39
|
* hot-path WS. Le firewall fournit l'implémentation (closure sur son container) ;
|
|
40
40
|
* absent par défaut (verrou pur, testable sans audit).
|
|
41
41
|
*
|
|
42
|
-
* @param surface - cible refusée : pont
|
|
42
|
+
* @param surface - cible refusée : pont socket (`api.request`) ou canal
|
|
43
43
|
* (subscribe/inbound).
|
|
44
44
|
* @param target - pathname (api.request) ou nom de canal refusé.
|
|
45
45
|
* @param reason - raison machine stable (`zone_protected` | `channel_policy`).
|
|
@@ -159,7 +159,7 @@ export declare function buildSystemRules(prefixes: readonly string[]): readonly
|
|
|
159
159
|
* (api.request) ou la politique du canal (subscribe/inbound).
|
|
160
160
|
*
|
|
161
161
|
* Trois surfaces gardées :
|
|
162
|
-
* - `api.request {path}` (pont
|
|
162
|
+
* - `api.request {path}` (pont socket) : re-match de zone HTTP → zone
|
|
163
163
|
* protégée + anonyme = refus (autorisation de ZONE, identique à `GET {path}` ;
|
|
164
164
|
* la méthode/action est gardée en aval par le router + `@IsGranted`).
|
|
165
165
|
* - `subscribe {channel}` : politique de canal (système plancher + déclaration
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type IChannelPolicyResolver } from "../src/realtime/frameAuthorizer.js";
|
|
2
|
+
import type { FrameAuthorizer, IRealtimeToken } from "../src/realtime/realtimeContracts.js";
|
|
3
|
+
/** L'identité d'essai : qui se connecte, avec quels droits. */
|
|
4
|
+
export interface ITestIdentityOptions {
|
|
5
|
+
/** Identifiant de l'utilisateur (défaut `"alice"`). */
|
|
6
|
+
readonly identifier?: string;
|
|
7
|
+
/** Rôles portés (défaut `["ROLE_USER"]`) — ceux qu'aurait résolus le pare-feu. */
|
|
8
|
+
readonly roles?: readonly string[];
|
|
9
|
+
/** Scopes portés (défaut aucun) — l'axe des jetons d'API, pas de la session. */
|
|
10
|
+
readonly scopes?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* L'utilisateur rendu par `@CurrentUser()` et `RequestContext.getUser()`
|
|
13
|
+
* (défaut `{ identifier, roles }`). Passer l'entité de l'application quand
|
|
14
|
+
* l'action en lit d'autres champs.
|
|
15
|
+
*/
|
|
16
|
+
readonly user?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Une identité de connexion AUTHENTIFIÉE, telle que le pare-feu l'aurait posée
|
|
20
|
+
* au handshake.
|
|
21
|
+
*
|
|
22
|
+
* Pour éprouver un visiteur anonyme, ne rien passer au harnais : il pose alors
|
|
23
|
+
* le jeton anonyme, comme le repli Zero Trust du runtime.
|
|
24
|
+
*
|
|
25
|
+
* @param options - identifiant, rôles, scopes, utilisateur.
|
|
26
|
+
* @returns un jeton à passer en `identity` au harnais temps réel.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createTestIdentity(options?: ITestIdentityOptions): IRealtimeToken;
|
|
29
|
+
/** Écarts au pare-feu d'essai. */
|
|
30
|
+
export interface ITestFrameAuthorizerOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Hiérarchie de rôles, à la forme de `roleHierarchy` de la configuration de
|
|
33
|
+
* sécurité (défaut : aucune). La reprendre de l'application quand un rôle en
|
|
34
|
+
* couvre un autre — `{ ROLE_ADMIN: ["ROLE_USER"] }`.
|
|
35
|
+
*/
|
|
36
|
+
readonly roleHierarchy?: Record<string, readonly string[]>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Le VRAI verrou de frame du pare-feu, pour le harnais temps réel.
|
|
40
|
+
*
|
|
41
|
+
* C'est lui qui applique `@RealtimeChannel(nom, { roles })`,
|
|
42
|
+
* `@RealtimeAction(nom, { roles })` et les canaux système : sans lui, une
|
|
43
|
+
* politique déclarée n'est appliquée par personne — en test comme au runtime.
|
|
44
|
+
*
|
|
45
|
+
* @param channels - ce qui connaît les politiques déclarées : le hub temps réel
|
|
46
|
+
* (`getRealtimeHub()`), lu à CHAQUE frame — les politiques que le controller
|
|
47
|
+
* enregistre à sa construction y sont donc vues.
|
|
48
|
+
* @param options - hiérarchie de rôles de l'application.
|
|
49
|
+
* @returns le verrou à passer en `frameAuthorizer` au harnais.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createTestFrameAuthorizer(channels: IChannelPolicyResolver, options?: ITestFrameAuthorizerOptions): FrameAuthorizer;
|
package/docs/webhooks.md
CHANGED
|
@@ -893,6 +893,15 @@ runtime, zéro cycle. Pour valider ton implémentation, importe le banc de contr
|
|
|
893
893
|
(`tests/support/webhookPaginationContract.ts`) et branche-le sur ton store : les invariants d'ordre,
|
|
894
894
|
de filtres et de bornes sont alors prouvés, pas supposés.
|
|
895
895
|
|
|
896
|
+
**Ta fabrique lève quand son backend n'est pas joignable — jamais un registre qui se tait.** Un
|
|
897
|
+
`IWebhookStore` qui, faute de connexion, rend une liste d'abonnements vide ne lève rien et ne
|
|
898
|
+
livre plus rien : les abonnés cessent d'être notifiés, sans une ligne au journal, pendant que la
|
|
899
|
+
configuration affiche ton backend. Le registre livré pour SQL fait l'inverse : sa fabrique passe
|
|
900
|
+
par `resolveConnectedOrm()` (`drizzle/nodefony/registerStores.ts#resolveConnectedOrm`), qui lève
|
|
901
|
+
en nommant la cause — module absent, ordre de démarrage, dialecte — si la connexion n'existe pas.
|
|
902
|
+
Fais de même : vérifie la connexion dans la fabrique, et laisse remonter l'erreur. Ne la rattrape
|
|
903
|
+
pas dans chaque méthode par un `if (!connected) return []`.
|
|
904
|
+
|
|
896
905
|
## 📜 Normes appliquées
|
|
897
906
|
|
|
898
907
|
| Domaine | Norme / référence | Ancrage |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/security",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.5",
|
|
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",
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
"types": "./dist/types/index.d.ts",
|
|
17
17
|
"import": "./dist/index.js",
|
|
18
18
|
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./testing": {
|
|
21
|
+
"nodefony-source": "./nodefony/testing/index.ts",
|
|
22
|
+
"types": "./dist/types/nodefony/testing/index.d.ts",
|
|
23
|
+
"import": "./dist/nodefony/testing/index.js",
|
|
24
|
+
"default": "./dist/nodefony/testing/index.js"
|
|
19
25
|
}
|
|
20
26
|
},
|
|
21
27
|
"scripts": {
|
|
@@ -55,22 +61,22 @@
|
|
|
55
61
|
"zod": "^4.6.7"
|
|
56
62
|
},
|
|
57
63
|
"devDependencies": {
|
|
58
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
59
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
60
|
-
"@nodefony/user": "^10.0.0-beta.
|
|
64
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
65
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
66
|
+
"@nodefony/user": "^10.0.0-beta.5",
|
|
61
67
|
"@types/node": "26.6.5",
|
|
62
68
|
"@vitest/coverage-v8": "5.0.5",
|
|
63
|
-
"nodefony": "^10.0.0-beta.
|
|
69
|
+
"nodefony": "^10.0.0-beta.5",
|
|
64
70
|
"vitest": "5.0.5"
|
|
65
71
|
},
|
|
66
72
|
"license": "Apache-2.0",
|
|
67
73
|
"readmeFilename": "README.md",
|
|
68
74
|
"contributors": [],
|
|
69
75
|
"peerDependencies": {
|
|
70
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
71
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
72
|
-
"@nodefony/user": "^10.0.0-beta.
|
|
73
|
-
"nodefony": "^10.0.0-beta.
|
|
76
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
77
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
78
|
+
"@nodefony/user": "^10.0.0-beta.5",
|
|
79
|
+
"nodefony": "^10.0.0-beta.5"
|
|
74
80
|
},
|
|
75
81
|
"files": [
|
|
76
82
|
"dist",
|