@nodefony/realtime 10.0.0-alpha.3 → 10.0.0-alpha.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.
package/README.md CHANGED
@@ -204,4 +204,4 @@ expect(h.denials().map((d) => d.channel)).toEqual(["chat:admin"]); // refusé
204
204
 
205
205
  ## Licence
206
206
 
207
- CeCILL-B — Christophe CAMENSULI.
207
+ Apache 2.0 — Christophe CAMENSULI.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.149.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.148.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.149.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
@@ -15,8 +15,8 @@ import { ClusterBackplane, processIpcTransport } from "./nodefony/src/backplane/
15
15
  import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } from "./nodefony/src/backplane/backplaneRegistry.js";
16
16
  import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
17
17
  import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
18
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
19
- import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
18
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
19
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
20
20
  import { Kernel, Module, services, withTimeout } from "nodefony";
21
21
  //#region index.ts
22
22
  /**
@@ -5,7 +5,7 @@ import { createSyslogUplinkHandler } from "./syslogUplink.js";
5
5
  import { WsConnectionTransport } from "../transport/WsConnectionTransport.js";
6
6
  import { welcomeEnv } from "./welcomeEnv.js";
7
7
  import { deniedDetail } from "./deniedDetail.js";
8
- import { JsonRpcPeer, PLATFORM_INBOUND, RequestContext, RpcEnvelope, RpcError } from "nodefony";
8
+ import { JsonRpcPeer, PLATFORM_INBOUND, RequestContext, RpcEnvelope, RpcError, identityHint } from "nodefony";
9
9
  import { readBackpressureOptions } from "@nodefony/http";
10
10
  import { Controller } from "@nodefony/framework";
11
11
  //#region nodefony/src/server/RealtimeController.ts
@@ -269,10 +269,11 @@ var RealtimeController = class extends Controller {
269
269
  if (f.id !== void 0) return;
270
270
  const channel = f.method === "subscribe" ? typeof f.params?.channel === "string" ? f.params.channel : void 0 : typeof f.method === "string" ? f.method : void 0;
271
271
  if (channel !== void 0) {
272
+ const hint = identityHint(this.kernel);
272
273
  const denied = {
273
274
  channel,
274
275
  reason: "forbidden",
275
- ...deniedDetail(this.kernel?.environment, `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend`)
276
+ ...deniedDetail(this.kernel?.environment, `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend` + (hint === null ? "" : `. ${hint}`))
276
277
  };
277
278
  auditedPeer.notify("realtime:denied", denied);
278
279
  }
@@ -74,7 +74,6 @@ declare class Realtime extends Module<IRealtimeConfig> {
74
74
  export default Realtime;
75
75
  export { Realtime };
76
76
  export { RealtimeController, ServerRealtimeSocket, serverSocket, RealtimeHub, getRealtimeHub, SLOW_CONSUMER_BYTES, RESERVED_SYSTEM_PREFIXES, isReservedSystemChannel, LoopbackBackplane, ClusterBackplane, RedisBackplane, createRedisServiceTransport, resolveRedisChannel, resolveBackplaneOriginId, REDIS_RT_CHANNEL, registerBackplaneDriver, getBackplaneDriver, listBackplaneDrivers, processIpcTransport, ClusterProbeClient, setClusterProbeClient, processProbeTransport, clusterProbeHealth, clusterProbeRequestEnrich, clusterProbeInstance, mergeClusterHealth, WsConnectionTransport, createRealtimeAdminApi, buildRealtimeHealth, buildOwnHealth, RealtimeService, };
77
- export type { RealtimeConfig } from "./nodefony/config/config.js";
78
77
  export { ANONYMOUS_REALTIME_TOKEN } from "./nodefony/src/server/AnonymousRealtimeToken.js";
79
78
  export { defineRealtimeConfig, realtimeConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
80
79
  export type { IRealtimeConfig, IRealtimeConfigInput, } from "./nodefony/config/defineModuleConfig.js";
@@ -34,10 +34,10 @@ export declare const realtimeConfigSchema: z.ZodObject<{
34
34
  }, z.core.$strict>>;
35
35
  }, z.core.$strict>;
36
36
  /** Type de sortie (config normalisée + défauts appliqués). */
37
- export type RealtimeConfig = z.infer<typeof realtimeConfigSchema>;
37
+ export type IRealtimeSchemaConfig = z.infer<typeof realtimeConfigSchema>;
38
38
  /**
39
39
  * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
40
40
  * valides par construction ; passés au `super(..., config)` du Module class.
41
41
  */
42
- declare const config: RealtimeConfig;
42
+ declare const config: IRealtimeSchemaConfig;
43
43
  export default config;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { realtimeConfigSchema, type RealtimeConfig } from "./config.js";
2
+ import { realtimeConfigSchema, type IRealtimeSchemaConfig } from "./config.js";
3
3
  import type { IBackplane } from "../interfaces/IBackplane.js";
4
4
  /**
5
5
  * Builder type-safe de la configuration de `@nodefony/realtime`.
@@ -42,8 +42,8 @@ export declare function realtimeConfigJsonSchema(): unknown;
42
42
  /** Entrée du builder (champs avec défaut optionnels). */
43
43
  export type IRealtimeConfigInput = z.input<typeof realtimeConfigSchema>;
44
44
  /** Config normalisée et gelée (sortie du builder, lue par `RealtimeService`). */
45
- export type IRealtimeConfig = RealtimeConfig & {
46
- backplane: RealtimeConfig["backplane"] & {
45
+ export type IRealtimeConfig = IRealtimeSchemaConfig & {
46
+ backplane: IRealtimeSchemaConfig["backplane"] & {
47
47
  instance?: IBackplane;
48
48
  };
49
49
  };
package/docs/actions.md CHANGED
@@ -359,7 +359,7 @@ donc réservée, quoi qu'en dise son décorateur. À savoir avant de nommer une
359
359
 
360
360
  Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
361
361
  configuration de sécurité — la même liste que pour les canaux
362
- (`realtimeChannels`, `security/nodefony/config/config.ts:1093`) :
362
+ (`realtimeChannels`, `security/nodefony/config/config.ts:1124`) :
363
363
 
364
364
  ```ts ignore
365
365
  use("@nodefony/security", {
@@ -509,7 +509,7 @@ il sera conçu avec son premier consommateur réel.
509
509
  Un cas particulier mérite d'être connu avant d'écrire une action : **elle existe peut-être déjà en
510
510
  HTTP**. Le pont API expose la méthode `api.request`, qui rejoue une route de contrôleur sur la
511
511
  socket, avec la même garde et le même résultat qu'en REST — `invokeApiRequest()`
512
- (`RealtimeController.ts:818`). Il est **désactivé par défaut** et s'active en surchargeant
512
+ (`RealtimeController.ts:878`). Il est **désactivé par défaut** et s'active en surchargeant
513
513
  `realtimeApiRequest()` (`RealtimeController.ts:219`).
514
514
 
515
515
  ```ts ignore
@@ -126,7 +126,7 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
126
126
  Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
127
127
  classe `JsonRpcPeer` (`JsonRpcPeer.ts:271`) est du code isomorphe du cœur — le navigateur
128
128
  l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
129
- `RealtimeController.onHandshake()` (`RealtimeController.ts:312`). Le serveur peut donc
129
+ `RealtimeController.onHandshake()` (`RealtimeController.ts:328`). Le serveur peut donc
130
130
  appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
131
131
  déguisé.
132
132
 
@@ -141,7 +141,7 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
141
141
  et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
142
142
  toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
143
143
  multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
144
- `RealtimeController.requestClient()` (`RealtimeController.ts:263`).
144
+ `RealtimeController.requestClient()` (`RealtimeController.ts:279`).
145
145
 
146
146
  **Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
147
147
  santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
@@ -154,7 +154,7 @@ chaîne de caractères. Passer de mono-process à multi-pods ne touche aucune li
154
154
 
155
155
  **Le compromis, dit franchement** : un canal **ne franchit PAS** la frontière du process
156
156
  tant que tu ne l'as pas déclaré. Le défaut est l'isolement (`RealtimeHub.publish()`,
157
- `RealtimeHub.ts:530`) — un choix de sûreté détaillé plus bas.
157
+ `RealtimeHub.ts:604`) — un choix de sûreté détaillé plus bas.
158
158
 
159
159
  ## 🚀 Démarrage rapide
160
160
 
@@ -388,7 +388,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
388
388
 
389
389
  ## 🔌 Le cycle de vie d'une connexion
390
390
 
391
- Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:312`), appelé
391
+ Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:328`), appelé
392
392
  une seule fois par connexion, en chemin froid.
393
393
 
394
394
  ```mermaid
@@ -419,15 +419,15 @@ sequenceDiagram
419
419
 
420
420
  Les étapes, dans l'ordre exact du code :
421
421
 
422
- 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:896`). Refus →
422
+ 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:970`). Refus →
423
423
  fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
424
424
  2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
425
- (`RealtimeHub.ts:871`) parcourt les authentificateurs enregistrés : **le premier motif
425
+ (`RealtimeHub.ts:945`) parcourt les authentificateurs enregistrés : **le premier motif
426
426
  qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
427
427
  (`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
428
428
  échec d'authentification ferme en `4001`.
429
429
  3. **Création du peer et du transport**, puis association `peer → jeton`
430
- (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:928`), stockée dans une `WeakMap` : le
430
+ (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1002`), stockée dans une `WeakMap` : le
431
431
  jeton disparaît avec le peer, sans fuite.
432
432
  4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
433
433
  de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
@@ -459,13 +459,13 @@ déconnexion.
459
459
  La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
460
460
  figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
461
461
  déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
462
- registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:704`) — relit
462
+ registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:778`) — relit
463
463
  périodiquement ces identités et coupe les sockets orphelines. Seules les identités
464
464
  **révocables** y entrent : un visiteur anonyme ne coûte rien.
465
465
 
466
466
  ## Le hub — canaux partagés et fan-out
467
467
 
468
- Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1255`). Il ne
468
+ Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1435`). Il ne
469
469
  dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
470
470
  dépendances.
471
471
 
@@ -478,10 +478,10 @@ dépendances.
478
478
  paquet du producteur atteigne bien ce premier abonné.
479
479
  3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
480
480
  registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
481
- `RealtimeHub.ts:1117`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
481
+ `RealtimeHub.ts:1281`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
482
482
  l'abonnement est refusé et rien n'est alloué.
483
483
 
484
- Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:502`) appelle le
484
+ Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
485
485
  `dispose` du producteur et retire le canal. Un producteur fautif qui lève une exception ne
486
486
  bloque pas le nettoyage.
487
487
 
@@ -496,12 +496,12 @@ Deux méthodes qui se ressemblent et ne font pas du tout la même chose :
496
496
 
497
497
  | Méthode | Fan-out local | Propagation aux autres process | Qui l'appelle |
498
498
  | --------------------------------------- | :-----------: | :--------------------------------: | ----------------------------------- |
499
- | `publish()` (`RealtimeHub.ts:530`) | oui | oui, **si** le canal est broadcast | producteurs, contrôleurs, services |
500
- | `publishLocal()` (`RealtimeHub.ts:549`) | oui | **jamais** | l'arrivée d'un message du backplane |
499
+ | `publish()` (`RealtimeHub.ts:604`) | oui | oui, **si** le canal est broadcast | producteurs, contrôleurs, services |
500
+ | `publishLocal()` (`RealtimeHub.ts:623`) | oui | **jamais** | l'arrivée d'un message du backplane |
501
501
 
502
502
  C'est **la** règle qui empêche la tempête : un message reçu d'un pair est réinjecté
503
503
  localement et **ne repart pas**. Le câblage se fait une fois pour toutes dans
504
- `RealtimeHub.setBackplane()` (`RealtimeHub.ts:640`), qui branche l'arrivée du backplane
504
+ `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), qui branche l'arrivée du backplane
505
505
  directement sur `publishLocal`.
506
506
 
507
507
  Le fan-out lui-même (`RealtimeHub.ts:340`) est **isolé** : chaque livraison est protégée,
@@ -743,13 +743,13 @@ Deux propriétés architecturales méritent d'être notées ici, parce qu'elles
743
743
  choix de conception visibles partout dans le module :
744
744
 
745
745
  **Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
746
- si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:961`, testé une
746
+ si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1042`, testé une
747
747
  fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
748
748
  chaud ne paie **rien** du tout.
749
749
 
750
750
  **Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
751
751
  politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
752
- (`RealtimeHub.ts:1020`) le détecte et un avertissement est émis une fois par process
752
+ (`RealtimeHub.ts:1101`) le détecte et un avertissement est émis une fois par process
753
753
  (`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
754
754
  bien plus dangereux qu'un canal ouvertement public.
755
755
 
@@ -808,7 +808,7 @@ révocables, authentificateurs, politiques de canal, association peer→jeton :
808
808
  `null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
809
809
 
810
810
  **Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
811
- dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:719`).
811
+ dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:793`).
812
812
  Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
813
813
  process.
814
814
 
@@ -457,13 +457,13 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
457
457
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
458
458
 
459
459
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
460
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:896`). Une origine refusée ferme la socket avec le code
460
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:970`). Une origine refusée ferme la socket avec le code
461
461
  `4003`.
462
462
 
463
463
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
464
464
  active-le dès que ta socket est joignable depuis un navigateur.
465
465
 
466
- Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts:291`) :
466
+ Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts:313`) :
467
467
 
468
468
  | Situation | Résultat |
469
469
  | ------------------------------------------------ | ------------------------------------------------------------------------ |
@@ -1598,19 +1598,19 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1598
1598
  | Ce que tu as utilisé | Où c'est écrit |
1599
1599
  | --- | --- |
1600
1600
  | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:142`), `RealtimeInbound()` (`realtimeDecorators.ts:182`) |
1601
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:233`) |
1601
+ | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:249`) |
1602
1602
  | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:219`) |
1603
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`), `RealtimeController.startChannel()` (`RealtimeController.ts:618`) |
1604
- | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:530`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:549`) |
1603
+ | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`), `RealtimeController.startChannel()` (`RealtimeController.ts:746`) |
1604
+ | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:549`) |
1605
1605
  | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:594`) |
1606
- | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:640`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1606
+ | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1607
1607
  | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1037`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1037`) |
1608
1608
  | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:253`) |
1609
1609
  | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:228`), erreur `unauthorized` (`JsonRpcPeer.ts:400`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:349`) |
1610
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:736`) |
1610
+ | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
1611
1611
  | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:56`), fermeture `slow consumer` (`RealtimeHub.ts:63`) |
1612
- | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:910`) |
1613
- | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:204`) |
1612
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:984`) |
1613
+ | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1614
1614
  | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:775`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1615
1615
  | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1616
1616
  | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:200`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
265
265
  | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
266
266
  | `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
267
267
  | `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
268
- | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:764`). |
268
+ | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:838`). |
269
269
  | `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
270
270
 
271
271
  Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
package/docs/protocole.md CHANGED
@@ -347,7 +347,7 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
347
347
 
348
348
  > [!TIP]
349
349
  > **L'accueil est ta carte du territoire.** `methods` et `channels` sont construits à partir de ce
350
- > que l'endpoint expose réellement (`RealtimeController.ts:566`) : un client peut activer ou griser
350
+ > que l'endpoint expose réellement (`RealtimeController.ts:641`) : un client peut activer ou griser
351
351
  > ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
352
352
  > `socket.serverChannels`.
353
353
 
package/docs/securite.md CHANGED
@@ -118,8 +118,8 @@ politiques.**
118
118
  `security/nodefony/config/config.ts:109`). L'opt-out est explicite ; un opt-in aurait été
119
119
  _fail-open_ (une zone qui oublie le flag laisserait le WS anonyme).
120
120
  - Le verrou de frame consulte la **même** fonction de match que le HTTP, `Firewall.matchPath()`
121
- (`firewall.ts:696`), et la **même** hiérarchie de rôles, `Firewall.hasRole()`
122
- (`firewall.ts:466`). Invariant par construction : `api.request {path}` n'accorde jamais plus que
121
+ (`firewall.ts:712`), et la **même** hiérarchie de rôles, `Firewall.hasRole()`
122
+ (`firewall.ts:482`). Invariant par construction : `api.request {path}` n'accorde jamais plus que
123
123
  `GET {path}`.
124
124
  - L'identité du handshake est celle du firewall HTTP : `FirewallRealtimeAuthenticator`
125
125
  (`FirewallRealtimeAuthenticator.ts:57`) ne relit ni cookie ni base, il **promeut** l'`IUser` déjà
@@ -263,12 +263,12 @@ Nodefony pose **deux barrières successives**, à deux étages différents.
263
263
  **Barrière 1 — transport, active sans rien faire.** `HttpKernel.checkWebsocketOrigin()`
264
264
  (`http-kernel.ts:599`) exige que l'`Origin` du handshake corresponde au `Host` servi, avec tolérance
265
265
  loopback en développement et une allowlist optionnelle (`allowedOrigins`,
266
- `http/nodefony/config/config.ts:525`) acceptant le hostname exact ou un wildcard à un label. Une
266
+ `http/nodefony/config/config.ts:552`) acceptant le hostname exact ou un wildcard à un label. Une
267
267
  requête **sans** `Origin` est acceptée : un attaquant non-navigateur n'a pas besoin de CSWSH.
268
268
 
269
269
  **Barrière 2 — module realtime, opt-in et plus stricte.** `RealtimeHub.checkOrigin()`
270
- (`RealtimeHub.ts:896`) consulte une garde compilée une fois au boot par `buildOriginGuard()`
271
- (`RealtimeService.ts:291`) depuis `checkOriginSchema` (`realtime/nodefony/config/config.ts:162`).
270
+ (`RealtimeHub.ts:970`) consulte une garde compilée une fois au boot par `buildOriginGuard()`
271
+ (`RealtimeService.ts:313`) depuis `checkOriginSchema` (`realtime/nodefony/config/config.ts:162`).
272
272
  Trois différences comptent :
273
273
 
274
274
  1. **Match exact scheme + host + port**, aucun wildcard. Durcissement volontaire par rapport au
@@ -292,13 +292,13 @@ n'est jamais traitée.
292
292
 
293
293
  ### Le pipeline exact
294
294
 
295
- `RealtimeController.onHandshake()` (`RealtimeController.ts:312`) exécute, une fois par connexion :
295
+ `RealtimeController.onHandshake()` (`RealtimeController.ts:328`) exécute, une fois par connexion :
296
296
 
297
297
  1. Construction d'un DTO neutre `IRealtimeHandshake` par `buildHandshakeFromContext()`
298
- (`RealtimeController.ts:1053`) — headers, cookies aplatis, url, origin, sous-protocoles. Aucune
298
+ (`RealtimeController.ts:1113`) — headers, cookies aplatis, url, origin, sous-protocoles. Aucune
299
299
  dépendance à `@nodefony/security` dans le contrat.
300
300
  2. Contrôle d'origine (verrou 1).
301
- 3. Résolution de l'authenticator par `RealtimeHub.resolveAuthenticator()` (`RealtimeHub.ts:871`) :
301
+ 3. Résolution de l'authenticator par `RealtimeHub.resolveAuthenticator()` (`RealtimeHub.ts:945`) :
302
302
  les matchers sont testés dans l'ordre d'enregistrement, **le premier qui matche capture**.
303
303
  4. `authenticator.authenticate(handshake)` — **async autorisé** (on est en cold path, une fois par
304
304
  connexion : lire un store est acceptable ici, jamais par frame).
@@ -312,8 +312,8 @@ d'audit défectueux ne peut pas empêcher la fermeture.
312
312
 
313
313
  ### Les matchers — quelle porte, quel vigile
314
314
 
315
- `RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:853`) associe un sélecteur à une stratégie.
316
- `compileMatcher()` (`RealtimeHub.ts:855`) le compile une fois :
315
+ `RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:927`) associe un sélecteur à une stratégie.
316
+ `compileMatcher()` (`RealtimeHub.ts:1402`) le compile une fois :
317
317
 
318
318
  - `pattern` chaîne → RegExp **préfixe ancrée**, méta-caractères échappés par `RegExp.escape`.
319
319
  `"/admin/"` matche `/admin/` et `/admin/foo`, littéralement.
@@ -330,7 +330,7 @@ identité d'instance et une instance partagée n'enregistrerait que le premier m
330
330
 
331
331
  ### Zero Trust — il y a toujours un token
332
332
 
333
- `RealtimeHub.getTokenForPeer()` (`RealtimeHub.ts:940`) ne renvoie **jamais** `null` : à défaut de
333
+ `RealtimeHub.getTokenForPeer()` (`RealtimeHub.ts:1014`) ne renvoie **jamais** `null` : à défaut de
334
334
  token posé, c'est `ANONYMOUS_REALTIME_TOKEN` (`AnonymousRealtimeToken.ts:18`), singleton gelé,
335
335
  `isAuthenticated() === false`, `roles: ["ROLE_ANONYMOUS"]`. Le code consommateur (verrou, audit)
336
336
  n'a jamais à écrire « et s'il n'y a pas de token ? ».
@@ -468,11 +468,11 @@ Trois durcissements méritent d'être connus :
468
468
  `authenticated: true`. Le test porte sur le **namespace du canal**, pas sur le préfixe de la règle
469
469
  qui a matché : un préfixe de config plus court ou altéré ne contourne rien.
470
470
  - **La config passe avant les défauts** — les règles de `realtimeChannels`
471
- (`security/nodefony/config/config.ts:1093`) sont placées en tête, premier match gagnant. On peut
471
+ (`security/nodefony/config/config.ts:1124`) sont placées en tête, premier match gagnant. On peut
472
472
  donc re-cibler `nodefony:syslog` sur `ROLE_SECURITY_AUDITOR` ; on ne peut pas l'ouvrir à l'anonyme.
473
473
 
474
474
  Le canal du journal d'audit (`nodefony:audit`) est enregistré comme **canal système** sur le hub
475
- (`RealtimeHub.registerSystemChannel()`, `RealtimeHub.ts:1117`) : il devient servable par n'importe
475
+ (`RealtimeHub.registerSystemChannel()`, `RealtimeHub.ts:1281`) : il devient servable par n'importe
476
476
  quel endpoint realtime, sans qu'aucun controller ne le connaisse — et il est gardé par sa règle
477
477
  dédiée. Son enregistrement est **couplé** à la pose du verrou (même condition), donc il n'existe
478
478
  jamais de canal d'audit non gardé.
@@ -486,12 +486,12 @@ cumulatifs (ET) ; un axe absent n'impose rien :
486
486
  | Axe | Sens | Évalué par |
487
487
  | --------------- | ----------------------------------------------------------------------------- | ---------------------------------------- |
488
488
  | `authenticated` | token non anonyme | `satisfies()` (`frameAuthorizer.ts:240`) |
489
- | `roles` | un des rôles suffit, **hiérarchie comprise** | `Firewall.hasRole()` (`firewall.ts:466`) |
489
+ | `roles` | un des rôles suffit, **hiérarchie comprise** | `Firewall.hasRole()` (`firewall.ts:482`) |
490
490
  | `scopes` | un des scopes suffit — axe API (JWT, clé API), une session BFF n'en porte pas | comparaison directe |
491
491
 
492
492
  Une policy **vide** n'est pas enregistrée : `definePolicy()` (`realtimeDecorators.ts:40`) ignore un
493
493
  objet sans contrainte — le canal reste libre, le registre reste vide. Les déclarations sont publiées
494
- au hub **au handshake**, pas au boot (`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts:1037`,
494
+ au hub **au handshake**, pas au boot (`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts:1127`,
495
495
  idempotent) ; le décideur les relit par `RealtimeService.resolveChannelPolicy()`
496
496
  (`RealtimeService.ts:277`).
497
497
 
@@ -508,7 +508,7 @@ là, le canal fonctionne, et le refus attendu n'arrive jamais.
508
508
 
509
509
  Le serveur le **dit** désormais. Au premier canal servi dynamiquement sans politique, alors que le
510
510
  module en déclare pour d'autres canaux, un avertissement est journalisé une fois
511
- (`RealtimeHub.noticeUnguardedDynamicChannel()`, `RealtimeHub.ts:1090`) :
511
+ (`RealtimeHub.noticeUnguardedDynamicChannel()`, `RealtimeHub.ts:1254`) :
512
512
 
513
513
  ```
514
514
  le canal "chat:room:1000" est servi par une fabrique DYNAMIQUE et n'est couvert par AUCUNE
@@ -537,14 +537,14 @@ C'est exactement le test de `Firewall.#wireRealtime()` (`firewall.ts:253`) : san
537
537
 
538
538
  Deuxième subtilité : `beforeDispatch` n'est branché sur une connexion que si le verrou est **déjà**
539
539
  posé au moment de son handshake (`RealtimeController.ts:429`, via
540
- `RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:961`). Choix de performance délibéré (un hub
540
+ `RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:1042`). Choix de performance délibéré (un hub
541
541
  non sécurisé garde un coût nul par frame), mais avec une conséquence : **une connexion ouverte avant
542
542
  la pose du verrou n'est jamais gardée**, et ce jusqu'à sa fermeture. En fonctionnement normal le
543
543
  firewall se construit au boot, avant tout trafic ; le cas ne se présente qu'en appelant
544
544
  `setFrameAuthorizer()` à chaud.
545
545
 
546
546
  **Le refus de dégrader en silence.** Quand des policies sont déclarées sans décideur câblé,
547
- `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1020`) renvoie `true` et le controller
547
+ `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1101`) renvoie `true` et le controller
548
548
  émet un WARNING explicite, une seule fois par process (`RealtimeController.ts:522`) :
549
549
 
550
550
  ```text
@@ -600,8 +600,8 @@ Nodefony ferme l'écart par deux mécanismes de granularité différente.
600
600
 
601
601
  | Surface | Re-validation | Fenêtre d'exposition | Où |
602
602
  | --------------------------- | -------------------- | -------------------- | --------------------------------------------------------------------- |
603
- | `api.request` (data plane) | **à chaque frame** | nulle | `RealtimeController.invokeApiRequest()` (`RealtimeController.ts:818`) |
604
- | `subscribe` / flux de canal | **périodique**, 30 s | ≤ 30 s | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:736`) |
603
+ | `api.request` (data plane) | **à chaque frame** | nulle | `RealtimeController.invokeApiRequest()` (`RealtimeController.ts:878`) |
604
+ | `subscribe` / flux de canal | **périodique**, 30 s | ≤ 30 s | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
605
605
 
606
606
  **Sur `api.request`**, `token.isValid()` est appelé avant l'exécution de l'action ; identité périmée
607
607
  ou changée → `-32000` avec `status: 401`, et le client bascule sur un `fetch` HTTP porteur du cookie
@@ -609,7 +609,7 @@ courant. Une erreur de re-validation vaut refus (fail-closed).
609
609
 
610
610
  **Sur les canaux**, le hub n'inscrit au registre de révocation que les connexions dont le token porte
611
611
  `isValid` (`RealtimeController.ts:550`) — anonymes et JWT n'y entrent jamais, coût nul.
612
- `RealtimeHub.registerRevocable()` (`RealtimeHub.ts:704`) démarre un `setInterval` `unref` au premier
612
+ `RealtimeHub.registerRevocable()` (`RealtimeHub.ts:778`) démarre un `setInterval` `unref` au premier
613
613
  inscrit et l'arrête dès que le registre se vide : zéro timer au repos. Période :
614
614
  `REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`), 30 s, alignée sur le heartbeat WS.
615
615
 
@@ -674,10 +674,10 @@ Un onglet en arrière-plan, un mobile en zone blanche, une fenêtre TCP pleine :
674
674
  grossit sans borne, et le multiplexage concentre le risque (une socket lente bloque tous ses
675
675
  canaux). `WsConnectionTransport.send()` (`WsConnectionTransport.ts:76`) applique deux seuils :
676
676
 
677
- | `bufferedAmount` | Action |
678
- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
679
- | > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:625`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
680
- | `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
677
+ | `bufferedAmount` | Action |
678
+ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
679
+ | > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1044`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
680
+ | `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
681
681
 
682
682
  ### Taille des messages entrants
683
683
 
@@ -724,7 +724,7 @@ Source unique des défauts : `realtimeConfigSchema` (`realtime/nodefony/config/c
724
724
  Cette section existe pour éviter une confiance mal placée. Chaque point est vérifié au code.
725
725
 
726
726
  **1. Il n'y a pas de frontière de canal entre modules.** Un nom de canal est une chaîne dans un
727
- registre global (`#channelPolicies` du hub singleton, `RealtimeHub.ts:295`). Rien n'empêche le
727
+ registre global (`#channelPolicies` du hub singleton, `RealtimeHub.ts:325`). Rien n'empêche le
728
728
  controller du module A de servir un canal au nom d'un canal du module B, ni de déclarer une policy
729
729
  plus faible sur ce même nom — la déclaration la plus récente écrase (`RealtimeHub.ts:751`, idempotent
730
730
  par écrasement). En pratique, l'exploitation exige que le controller fautif accepte le nom (sa