@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 API souverain) : re-match de zone HTTP → zone
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 API souverain (`api.request`) ou canal
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 API souverain) : re-match de zone HTTP → zone
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.4",
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.4",
59
- "@nodefony/http": "^10.0.0-beta.4",
60
- "@nodefony/user": "^10.0.0-beta.4",
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.4",
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.4",
71
- "@nodefony/http": "^10.0.0-beta.4",
72
- "@nodefony/user": "^10.0.0-beta.4",
73
- "nodefony": "^10.0.0-beta.4"
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",