@nodefony/realtime 10.0.0-alpha.1

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.
Files changed (81) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +207 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +218 -0
  6. package/dist/nodefony/config/config.js +63 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
  9. package/dist/nodefony/interfaces/IBackplane.js +1 -0
  10. package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
  11. package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
  12. package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
  13. package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
  14. package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
  15. package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
  16. package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
  17. package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
  18. package/dist/nodefony/interfaces/index.js +1 -0
  19. package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
  20. package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
  21. package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
  22. package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
  23. package/dist/nodefony/src/backplane/envelope.js +109 -0
  24. package/dist/nodefony/src/backplane/originId.js +35 -0
  25. package/dist/nodefony/src/backplane/publishQueue.js +96 -0
  26. package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
  27. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
  28. package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
  29. package/dist/nodefony/src/server/RealtimeController.js +601 -0
  30. package/dist/nodefony/src/server/RealtimeHub.js +926 -0
  31. package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
  32. package/dist/nodefony/src/server/deniedDetail.js +29 -0
  33. package/dist/nodefony/src/server/syslogUplink.js +105 -0
  34. package/dist/nodefony/src/server/welcomeEnv.js +25 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +240 -0
  36. package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
  37. package/dist/nodefony/testing/index.js +163 -0
  38. package/dist/types/index.d.ts +99 -0
  39. package/dist/types/nodefony/config/config.d.ts +43 -0
  40. package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
  41. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
  42. package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
  43. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
  44. package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
  45. package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
  46. package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
  48. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +18 -0
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
  53. package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
  54. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
  55. package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
  56. package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
  57. package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
  59. package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
  60. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
  61. package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
  65. package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
  66. package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
  67. package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
  68. package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
  69. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
  70. package/dist/types/nodefony/testing/index.d.ts +169 -0
  71. package/docs/actions.md +573 -0
  72. package/docs/architecture.md +889 -0
  73. package/docs/configuration.md +803 -0
  74. package/docs/cookbook-chat.md +1619 -0
  75. package/docs/couverture-bancs.md +139 -0
  76. package/docs/index.md +372 -0
  77. package/docs/observabilite.md +592 -0
  78. package/docs/protocole.md +518 -0
  79. package/docs/securite.md +845 -0
  80. package/docs/vocabulaire.md +845 -0
  81. package/package.json +83 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Port du **backplane** de la socket Nodefony — l'abstraction de fan-out
3
+ * **CROSS-PROCESS** du {@link RealtimeHub}.
4
+ *
5
+ * Le hub fait le fan-out **local** (aux connexions du process courant). Le backplane
6
+ * propage une publication aux **autres pairs** (autres workers d'un même pod via IPC,
7
+ * autres pods via Redis…) et réinjecte localement ce que les pairs publient. C'est
8
+ * **le même contrat** quel que soit le backing — c'est tout l'intérêt : on définit le
9
+ * port UNE fois et on prouve l'archi avec plusieurs implémentations interchangeables :
10
+ *
11
+ * - {@link LoopbackBackplane} — mono-process, **no-op** (aucun pair) ;
12
+ * - `ClusterBackplane` (IPC, master-gateway) — pairs = workers du même pod, SANS infra
13
+ * → « comme si Redis était là » : le banc d'essai qui stabilise l'archi multi-process ;
14
+ * - `RedisBackplane` (P13) — pairs = pods du cluster, **drop-in** (le hub ne change pas).
15
+ *
16
+ * Anti-boucle (règle absolue) : une publication locale part vers le backplane, mais un
17
+ * message **reçu** du backplane est réinjecté en **fan-out LOCAL UNIQUEMENT** (jamais
18
+ * re-publié vers le backplane, sinon tempête). Le hub applique cette barrière en
19
+ * appelant `RealtimeHub.publishLocal` dans son handler. Le backplane applique la 2ᵉ
20
+ * barrière : il **filtre son propre {@link originId}** (echo) avant d'appeler le handler.
21
+ *
22
+ * Sémantique de livraison : **best-effort / at-most-once** (comme un pub/sub) — aucune
23
+ * garantie de delivery ni d'ordre cross-pair ; le client realtime re-synchronise. Ne pas
24
+ * sur-concevoir une fiabilité que les backings (Redis pub/sub) n'offrent pas.
25
+ *
26
+ * Cf vision « la socket Nodefony » + mémoire `project_cluster_backplane_vision`.
27
+ */
28
+ /**
29
+ * Compteurs de la **file d'envoi** d'un backplane à transport réseau : ce qui est
30
+ * publié mais pas encore acquitté par le bus. Sans borne, cette file (interne au
31
+ * client réseau) grossit sans limite sous rafale — d'où le seuil et ses compteurs,
32
+ * exposés jusqu'à la sonde et Studio. Cf `BackplanePublishQueue`.
33
+ */
34
+ export interface IBackplaneQueueInfo {
35
+ /** Octets publiés en attente d'acquittement par le transport (instantané). */
36
+ bytes: number;
37
+ /** Seuil au-dessus duquel une publication est jetée ; `0` = illimité. */
38
+ maxBytes: number;
39
+ /** Publications JETÉES faute de place (monotone). `0` en régime sain. */
40
+ droppedTotal: number;
41
+ /** Publications REFUSÉES par le transport (bus coupé…) (monotone). */
42
+ failedTotal: number;
43
+ }
44
+ /**
45
+ * Message transporté entre pairs par le backplane. Sérialisé une fois à l'émission
46
+ * (IPC `structuredClone` / Redis JSON) ; le `payload` doit donc être structurellement
47
+ * clonable / sérialisable (pas de fonction, pas de cycle).
48
+ */
49
+ export interface IBackplaneMessage {
50
+ /** Canal logique (mêmes noms que côté hub, suffixe de cadence `:<ms>` inclus). */
51
+ channel: string;
52
+ /** Charge applicative diffusée sur le canal. */
53
+ payload: unknown;
54
+ /** Identité du pair **émetteur** — sert l'anti-echo (cf {@link IBackplane.originId}). */
55
+ originId: string;
56
+ }
57
+ /**
58
+ * Handler d'ingress : reçoit un message publié par un **autre** pair. L'echo du pair
59
+ * courant a déjà été filtré par l'implémentation → le handler peut réinjecter sans
60
+ * condition (mais en fan-out **local only**).
61
+ */
62
+ export type BackplaneHandler = (msg: IBackplaneMessage) => void;
63
+ /**
64
+ * **Carte d'identité** d'un backplane — descripteur de DONNÉE (pas un log) décrivant
65
+ * l'état du fond de panier. Source unique alimentant trois sorties cohérentes : la
66
+ * ligne stdout au boot, le champ `backplane` de la sonde (`/api/health`) et la vue
67
+ * Studio. Réutilisable par les tests / un agent IA sans dupliquer la connaissance
68
+ * « c'est quoi ce backplane ».
69
+ */
70
+ export interface IBackplaneInfo {
71
+ /** Nom du driver (clé du registre). Ex. `redis`, `cluster`, `loopback`. */
72
+ driver: string;
73
+ /** Nature du transport. Ex. `redis-pubsub`, `ipc`, `local`. */
74
+ kind: string;
75
+ /** Identité de CE pair (process/pod). Ex. `String(process.pid)`. */
76
+ originId: string;
77
+ /** `true` si le fan-out franchit les frontières de pod (multi-host). */
78
+ crossPod: boolean;
79
+ /** Canal/topic de transport, si applicable (canal Redis, topic Kafka). */
80
+ channel?: string;
81
+ /**
82
+ * Les messages sont-ils **scellés** (authenticité de l'émetteur vérifiée) ?
83
+ * Renseigné par les drivers à transport PARTAGÉ, où un tiers peut écrire :
84
+ * `false` y signale un bus ouvert. Absent quand la question n'a pas de sens
85
+ * (mono-process, IPC master↔workers — authentifié par construction).
86
+ */
87
+ sealed?: boolean;
88
+ /**
89
+ * État de la **file d'envoi** vers le bus (octets en vol, seuil, jetés). Renseigné
90
+ * par les drivers à transport RÉSEAU, dont les publications sont acquittées de
91
+ * façon asynchrone : c'est là qu'un bus lent fait grossir la mémoire du pod.
92
+ * `droppedTotal > 0` = du fan-out cross-pod a été sacrifié pour tenir la mémoire.
93
+ * Absent quand le transport n'a pas de file (mono-process, IPC synchrone).
94
+ */
95
+ queue?: IBackplaneQueueInfo;
96
+ }
97
+ /**
98
+ * Contrat d'un backplane realtime. Implémentation **stateless du point de vue métier**
99
+ * (l'état des canaux vit dans le hub) ; le backplane ne porte que le transport vers les
100
+ * pairs + son identité.
101
+ */
102
+ export interface IBackplane {
103
+ /** Identité stable de CE pair (process/pod). Discrimine l'echo. Ex. `String(process.pid)`. */
104
+ readonly originId: string;
105
+ /**
106
+ * Démarre le transport vers les pairs (connexion IPC/Redis, abonnements). Idempotent.
107
+ * Peut être synchrone (IPC) ou asynchrone (Redis) → retour `void | Promise<void>`.
108
+ */
109
+ start(): void | Promise<void>;
110
+ /**
111
+ * Propage une publication locale aux **autres** pairs. NE refait PAS le fan-out local
112
+ * (déjà fait par le hub). No-op s'il n'y a aucun pair (mono-process).
113
+ */
114
+ publish(channel: string, payload: unknown): void;
115
+ /**
116
+ * Enregistre le handler d'ingress (messages venus des autres pairs, echo déjà filtré).
117
+ * Un seul handler à la fois — un appel ultérieur remplace le précédent.
118
+ */
119
+ onMessage(handler: BackplaneHandler): void;
120
+ /** Arrête le transport et libère les ressources (connexions, listeners). Idempotent. */
121
+ stop(): void | Promise<void>;
122
+ /**
123
+ * Carte d'identité du backplane (driver, transport, origine, cross-pod, canal).
124
+ * Donnée pure — consommée par le log de boot, la sonde santé et Studio.
125
+ */
126
+ describe(): IBackplaneInfo;
127
+ }
128
+ export default IBackplane;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Politique d'autorisation d'un **canal** WebSocket — exigences à satisfaire
3
+ * pour s'y abonner (`subscribe`) ou y pousser (canal inbound full-duplex).
4
+ *
5
+ * Déclarée côté métier via {@link RealtimeChannel}/{@link RealtimeInbound}
6
+ * (`@RealtimeChannel("admin:x", { roles: ["ROLE_ADMIN"] })`), agrégée par le
7
+ * {@link RealtimeHub} et **lue par `@nodefony/security`** (P6) au moment de la
8
+ * frame via le seam `resolveChannelPolicy`. Le module realtime NE décide PAS de
9
+ * l'autorisation (il ne connaît ni la hiérarchie de rôles ni l'identité réelle) :
10
+ * il ne fait que **transporter la déclaration** jusqu'au décideur (le firewall).
11
+ *
12
+ * ⚠️ Cette interface est un **miroir structurel** de `IChannelPolicy` côté
13
+ * `@nodefony/security` (`realtimeContracts.ts`) — les deux modules ne s'importent
14
+ * pas (`package.json` disjoints). Le typage structurel de TS fait le pont à
15
+ * l'exécution. Toute dérive de forme casserait le seam silencieusement.
16
+ *
17
+ * Champs cumulatifs (ET) ; un champ absent = pas de contrainte sur cet axe ;
18
+ * objet entièrement vide = canal libre (équivaut à « pas de politique »).
19
+ */
20
+ export interface IChannelPolicy {
21
+ /** Exige une connexion authentifiée (token non anonyme). */
22
+ readonly authenticated?: boolean;
23
+ /** Un de ces rôles suffit (évalué AVEC la hiérarchie de rôles, côté security). */
24
+ readonly roles?: readonly string[];
25
+ /** Un de ces scopes suffit (axe API : JWT/clé API ; session BFF n'en porte pas). */
26
+ readonly scopes?: readonly string[];
27
+ }
28
+ export default IChannelPolicy;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bornes de la réception des journaux remontés par un navigateur.
3
+ *
4
+ * Elles vivent dans une interface à part parce qu'elles voyagent : la configuration du
5
+ * module les produit, le hub les porte, et le contrôleur les consomme au handshake pour
6
+ * fabriquer le handler d'une connexion. Le hub n'en DÉCIDE rien — il les transporte,
7
+ * comme il transporte la garde d'origine et le plafond de canaux.
8
+ *
9
+ * Leur présence vaut ouverture du canal : `null` sur le hub = aucun handler entrant
10
+ * déclaré pour `nodefony:syslog:uplink`, donc une frame qui l'invoque est droppée comme
11
+ * n'importe quelle méthode inconnue.
12
+ */
13
+ export interface IClientLogsLimits {
14
+ /** Entrées retenues par lot reçu ; le surplus est ignoré. */
15
+ readonly maxEntriesPerBatch: number;
16
+ /** Entrées retenues par fenêtre et par connexion. */
17
+ readonly maxEntriesPerWindow: number;
18
+ /** Durée de la fenêtre de débit, en ms. */
19
+ readonly windowMs: number;
20
+ /** Longueur maximale d'une chaîne acceptée (message, pile). */
21
+ readonly maxStringLength: number;
22
+ }
@@ -0,0 +1,53 @@
1
+ import type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
2
+ import type { IRealtimeToken } from "./IRealtimeToken.js";
3
+ /**
4
+ * Stratégie d'authentification au handshake WebSocket — équivalent realtime du
5
+ * pattern `IAuthenticator` côté HTTP de `@nodefony/security`.
6
+ *
7
+ * Plusieurs authenticators sont enregistrés sur le hub via
8
+ * `RealtimeService.useAuthenticator(matcher, authenticator)`. Au handshake, le
9
+ * hub itère les matchers ordonnés ; le **1ʳᵉ** qui matche capture la connexion,
10
+ * son authenticator est exécuté (`supports` puis `authenticate`).
11
+ *
12
+ * Implémentations futures (P6) : `AnonymousRealtimeAuthenticator`,
13
+ * `JwtRealtimeAuthenticator` (cookie HTTP-only), `ApiKeyRealtimeAuthenticator`.
14
+ *
15
+ * **SYNC vs ASYNC** — l'authentification PEUT être asynchrone (handshake = 1×
16
+ * par connexion, cold path). Le RÉSULTAT (`IRealtimeToken`) est ensuite caché
17
+ * par le hub sur le peer ; les hooks hot-path (`beforeDispatch`, `onFrameAudit`)
18
+ * le lisent en O(1) sans recalculer l'auth par frame (cf
19
+ * `JsonRpcPeerOptions.beforeDispatch` — doctrine sync stricte du dispatch).
20
+ *
21
+ * @see IRealtimeAuthenticatorMatcher
22
+ */
23
+ export interface IRealtimeAuthenticator {
24
+ /** Nom logique (référencé dans les matchers, ex. `"realtime_jwt"`). */
25
+ readonly name: string;
26
+ /**
27
+ * Peut-on extraire un credential de cette requête upgrade ? (sinon le hub
28
+ * pose un token anonyme par défaut).
29
+ */
30
+ supports(handshake: IRealtimeHandshake): boolean;
31
+ /**
32
+ * Valide le credential et renvoie un token AUTHENTIFIÉ.
33
+ *
34
+ * `throw` une erreur (typiquement `AuthenticationError`) → le hub ferme la
35
+ * connexion WebSocket avec un code 4001 (`unauthorized`, plage applicative
36
+ * RFC 6455 §7.4.2) et n'instancie pas de peer.
37
+ *
38
+ * En cas de succès, le token est POSÉ par le hub sur la map `peer → token`.
39
+ */
40
+ authenticate(handshake: IRealtimeHandshake): Promise<IRealtimeToken>;
41
+ /**
42
+ * Hook succès — appelé APRÈS pose du token sur le peer, AVANT le welcome
43
+ * JSON-RPC. Sert à journaliser (audit S1), poser un cookie de session si
44
+ * besoin (rare en WS), etc. `void` (pas de back-pressure handshake).
45
+ */
46
+ onSuccess?(handshake: IRealtimeHandshake, token: IRealtimeToken): void;
47
+ /**
48
+ * Hook échec — appelé APRÈS qu'`authenticate` a throw, AVANT la fermeture
49
+ * de la socket (code 4001). Permet de tracer l'échec sans interrompre la
50
+ * fermeture. Throw d'ici est ignoré (catch défensif côté hub).
51
+ */
52
+ onFailure?(handshake: IRealtimeHandshake, error: Error): void;
53
+ }
@@ -0,0 +1,46 @@
1
+ import type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
2
+ /**
3
+ * Sélecteur d'une zone WS protégée — équivalent realtime de `ISecuredArea`
4
+ * côté HTTP de `@nodefony/security` (pattern URL + vhost optionnel).
5
+ *
6
+ * Le hub stocke les matchers dans l'ordre d'enregistrement ; le **1ʳᵉ** dont
7
+ * `match()` renvoie `true` capture le handshake. Convention : enregistrer les
8
+ * patterns les plus spécifiques EN PREMIER.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Zone admin (préfixe + vhost) — JWT obligatoire
13
+ * realtimeService.useAuthenticator(
14
+ * { pattern: /^\/admin\//, host: "admin.example.com" },
15
+ * jwtAuthenticator,
16
+ * );
17
+ * // Zone publique chat — anonyme accepté
18
+ * realtimeService.useAuthenticator(
19
+ * { pattern: /^\/chat\// },
20
+ * anonymousAuthenticator,
21
+ * );
22
+ * ```
23
+ */
24
+ export interface IRealtimeAuthenticatorMatcher {
25
+ /**
26
+ * Pattern d'URL (path) — string compilé en RegExp (escape complet) OU RegExp
27
+ * fournie telle quelle. La RegExp est testée contre `handshake.url` (le path
28
+ * sans query string).
29
+ */
30
+ readonly pattern: string | RegExp;
31
+ /**
32
+ * Vhost (Host header) optionnel — match exact (insensible à la casse). Omis
33
+ * = tous domaines. Parité avec `ISecuredArea.host` HTTP.
34
+ */
35
+ readonly host?: string;
36
+ }
37
+ /**
38
+ * Forme compilée d'un matcher — utilisé en interne par le hub (cache de la
39
+ * RegExp compilée + host lowercase). Pas destiné à l'API publique.
40
+ */
41
+ export interface ICompiledRealtimeMatcher {
42
+ readonly pattern: RegExp;
43
+ readonly host?: string;
44
+ /** Le matcher capture-t-il ce handshake ? */
45
+ match(handshake: IRealtimeHandshake): boolean;
46
+ }
@@ -0,0 +1,36 @@
1
+ /** Publie une charge sur un canal (notification serveur→client). */
2
+ export type RealtimePublish = (channel: string, payload: unknown) => void;
3
+ /**
4
+ * Handler d'un message ENTRANT sur un canal full-duplex (client → serveur). C'est le
5
+ * seam des backings entrants (SIP, bridge) : le client `publish(channel, payload)` ; si
6
+ * le contrôleur a déclaré ce canal dans `realtimeInbound()`, le handler est appelé.
7
+ *
8
+ * - `params` = la charge du client. **NON FIABLE** (Zero Trust) : le handler DOIT la
9
+ * valider (origine non authentifiée au niveau transport).
10
+ * - `reply` = pousse une charge serveur→client sur le MÊME canal, vers CETTE connexion
11
+ * uniquement (ex. réponse SIP). Per-connexion (≠ fan-out).
12
+ *
13
+ * Par défaut aucun canal n'accepte d'entrée (sûr) : un contrôleur opte explicitement.
14
+ */
15
+ export type RealtimeInboundHandler = (params: unknown, reply: (payload: unknown) => void) => void;
16
+ /**
17
+ * IRealtimeController — contrat d'un contrôleur temps réel SERVEUR (endpoint WS
18
+ * JSON-RPC 2.0). Le protocole (discrimination, dispatch, actions, pending, pub/sub,
19
+ * cleanup) est porté par {@link RealtimeController} via un `JsonRpcPeer` PAR
20
+ * connexion ; le contrôleur concret ne fournit QUE ses providers de canaux.
21
+ *
22
+ * (Les hooks `realtimeActions()` / `realtimeChannels()` sont `protected` sur la base
23
+ * — détail d'implémentation surchargeable, hors contrat public.)
24
+ *
25
+ * Côté client, le pendant n'est PAS un controller mais `RealtimeClient` — tous deux
26
+ * composent le même peer ; cf vision « la socket Nodefony ».
27
+ */
28
+ export interface IRealtimeController {
29
+ /**
30
+ * Crée le provider d'un canal au `subscribe` (démarre listener/ticker, pousse via
31
+ * `publish`) et renvoie son `dispose` (appelé au `unsubscribe` ET à la fermeture).
32
+ * `null` si le canal est inconnu.
33
+ */
34
+ createRealtimeChannel(channel: string, publish: RealtimePublish): (() => void) | null;
35
+ }
36
+ export default IRealtimeController;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Données de handshake WebSocket — DTO minimal passé à un
3
+ * {@link IRealtimeAuthenticator} pour qu'il extraie un credential et valide
4
+ * l'identité, SANS coupler ce module à `@nodefony/http` runtime.
5
+ *
6
+ * Construit par `RealtimeController.onHandshake()` depuis le `WebsocketContext`
7
+ * (request upgrade HTTP) puis consommé une seule fois par connexion (cold path).
8
+ *
9
+ * Cookies : déjà PARSÉS en `Map<string, string>` (et non l'en-tête `Cookie` brut)
10
+ * — la plupart des authenticators stateless WS lisent un cookie JWT.
11
+ *
12
+ * @see IRealtimeAuthenticator
13
+ */
14
+ export interface IRealtimeHandshake {
15
+ /** En-têtes de la requête upgrade (clés en lowercase RFC 7230 §3.2). */
16
+ readonly headers: Readonly<Record<string, string | string[] | undefined>>;
17
+ /** Cookies parsés depuis `Cookie:` (vide si en-tête absent ou mal formé). */
18
+ readonly cookies: ReadonlyMap<string, string>;
19
+ /** URL de l'upgrade (pathname + query string), ex. `/realtime?token=...`. */
20
+ readonly url: string;
21
+ /** IP du client (déjà extraite — derrière proxy : voir `trust proxy`). */
22
+ readonly remoteAddress: string;
23
+ /** Origin RFC 6455 §10.2 — `undefined` si en-tête absent (clients non-browser). */
24
+ readonly origin?: string;
25
+ /** Sous-protocoles annoncés (`Sec-WebSocket-Protocol`). `[]` si aucun. */
26
+ readonly protocols: readonly string[];
27
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Contrats d'**auto-observabilité de la socket Nodefony** — la sonde SERVEUR du
3
+ * {@link RealtimeHub}. Applique le patron sondes+hub (comme les sondes ORM) à la
4
+ * couche realtime elle-même : « la socket s'observe à travers elle-même ».
5
+ *
6
+ * Trois familles de signaux, dérivées de l'analyse du multiplexing N canaux / 1 WS
7
+ * (le design est bon mais déplace 3 responsabilités sur le hub) :
8
+ * - **fan-out** : combien de canaux, d'abonnés, de livraisons (chatter du broker) ;
9
+ * - **backpressure** (risque #1, SEUL vrai blocker mémoire) : `bufferedAmount` par
10
+ * connexion — une connexion lente fait grossir la file `ws` → OOM, et le
11
+ * multiplexing CONCENTRE (1 WS lente bloque TOUS ses canaux) ;
12
+ * - **débit octets/frames** : matière à mesurer AVANT d'optimiser (stringify unique,
13
+ * seuil de drop). Tous les cumuls sont **monotones** → le débit/s se dérive côté
14
+ * lecteur (delta `total`/`ts`, comme le CPU% ou le flux ORM) : 0 état de lecture.
15
+ */
16
+ import type { IProcessHealth, IProcessRich, IOrmLeanHealth, IInstanceErrorHealth } from "nodefony";
17
+ import type { IBackplaneInfo } from "./IBackplane.js";
18
+ /** Vue d'UNE connexion realtime pour la sonde (backpressure = risque #1). */
19
+ export interface IRealtimeConnProbe {
20
+ /** État du transport (0..3, aligné `WebSocket.readyState`). */
21
+ readonly readyState: number;
22
+ /**
23
+ * Octets en file d'envoi NON encore drainés vers le réseau (`ws.bufferedAmount`).
24
+ * > 0 durablement = slow-consumer → la file interne grossit (borne = timeout TCP)
25
+ * → pression mémoire. C'est LE signal du blocker #1.
26
+ */
27
+ readonly bufferedAmount: number;
28
+ /** Cumul d'octets envoyés à cette connexion (monotone). */
29
+ readonly bytesSent: number;
30
+ /** Cumul de frames envoyées à cette connexion (monotone). */
31
+ readonly messagesSent: number;
32
+ /**
33
+ * Cumul de frames JETÉES par back-pressure sur cette connexion (monotone) :
34
+ * drop latest-wins (`bufferedAmount ≥` seuil DROP) + la frame en cours quand on
35
+ * coupe un slow-consumer (`close 1013`). `> 0` = ce client n'absorbe pas le flux.
36
+ */
37
+ readonly dropped: number;
38
+ }
39
+ /** Stat per-canal du hub (fan-out). */
40
+ export interface IRealtimeChannelStat {
41
+ /** Nom du canal (suffixe `:<ms>` de granularité inclus). */
42
+ channel: string;
43
+ /** Abonnés locaux vivants (sinks) — instantané. */
44
+ subscribers: number;
45
+ /** Publications cumulées sur ce canal (monotone) → débit dérivé côté lecteur. */
46
+ messages: number;
47
+ }
48
+ /**
49
+ * Snapshot PUR du hub (sans identité process) — ce que renvoie
50
+ * {@link RealtimeHub.probe}. Per-instance/pod (cloud-native) ; l'agrégat multi-pod
51
+ * = backplane Redis / Prometheus (P13).
52
+ */
53
+ export interface IRealtimeProbe {
54
+ /** Horodatage du snapshot (ms epoch) — base du calcul de débit côté lecteur. */
55
+ ts: number;
56
+ /** Canaux actifs (≥ 1 abonné) avec abonnés + publications cumulées. */
57
+ channels: IRealtimeChannelStat[];
58
+ /** Nombre de canaux actifs (= 1 provider/canal/pod). */
59
+ channelCount: number;
60
+ /** Appels `publish` cumulés (monotone). */
61
+ publishTotal: number;
62
+ /** Livraisons cumulées (`publish` × abonnés) — vrai coût de fan-out (monotone). */
63
+ fanoutTotal: number;
64
+ /** Frames entrantes full-duplex cumulées (canaux gated SIP/bridge) (monotone). */
65
+ inboundTotal: number;
66
+ /**
67
+ * Messages d'ingress backplane REFUSÉS (canal non déclaré broadcast) — monotone.
68
+ * Reste à 0 en fonctionnement normal : un pair légitime n'émet que des canaux
69
+ * broadcast. Un compteur qui décolle = pair mal configuré, ou écriture tierce
70
+ * dans un bus partagé (Redis mutualisé). Cf `RealtimeHub` (contrôle d'admission).
71
+ */
72
+ ingressRejectedTotal: number;
73
+ /**
74
+ * Abonnements clients REFUSÉS par le plancher des canaux de plateforme —
75
+ * monotone. Un canal réservé (`syslog:`, `orm:`…) a été demandé alors qu'aucun
76
+ * module de sécurité n'est chargé : sans identité vérifiable, le hub ferme.
77
+ * `> 0` signale une configuration incomplète (sécurité absente) autant qu'une
78
+ * tentative d'accès — dans les deux cas, quelque chose est à regarder.
79
+ */
80
+ systemFloorDeniedTotal: number;
81
+ /** Connexions realtime vivantes. */
82
+ connectionCount: number;
83
+ /** Somme cumulée des octets envoyés, toutes connexions (monotone). */
84
+ bytesSentTotal: number;
85
+ /** Somme cumulée des frames envoyées, toutes connexions (monotone). */
86
+ messagesSentTotal: number;
87
+ /** Backpressure agrégée (instantanée) — risque #1. */
88
+ backpressure: {
89
+ /** Pire `bufferedAmount` parmi les connexions (octets). */
90
+ maxBufferedAmount: number;
91
+ /** Somme des `bufferedAmount` (octets en attente process-wide). */
92
+ totalBufferedAmount: number;
93
+ /** Connexions au-dessus du seuil d'alerte slow-consumer. */
94
+ slowConsumers: number;
95
+ /**
96
+ * Cumul de frames jetées par back-pressure (drop latest-wins + close
97
+ * slow-consumer), toutes connexions (monotone) → taux de perte dérivé côté
98
+ * lecteur. `0` en régime sain ; croît = des clients lents sont contenus pour
99
+ * protéger la mémoire process (sinon file `ws` non bornée → OOM).
100
+ */
101
+ drops: number;
102
+ };
103
+ /**
104
+ * Carte d'identité du backplane effectif (driver, transport, cross-pod, canal).
105
+ * Reflète l'état RÉEL : `local` si le hub n'a aucun backplane (mono-process ou
106
+ * fallback fail-soft), sinon le descripteur du driver branché. Optionnel pour
107
+ * rétrocompat des consommateurs qui construisent un probe partiel.
108
+ */
109
+ backplane?: IBackplaneInfo;
110
+ }
111
+ /** Snapshot enrichi de l'identité d'instance — santé per-instance d'un worker/pod. */
112
+ export interface IRealtimeHealth extends IRealtimeProbe {
113
+ /** Identifiant de CE process/pod (per-instance). */
114
+ instanceId: string;
115
+ /**
116
+ * Santé PROCESS du worker (CPU/mém/event-loop) — additif : la grille « salle des
117
+ * machines » de la vue pod lit `instances[].process` par worker. Optionnel (absent si
118
+ * sonde process coupée) → les consommateurs realtime existants l'ignorent (non-breaking).
119
+ */
120
+ process?: IProcessHealth;
121
+ /**
122
+ * Sonde process **riche** (GC/heap-spaces/handles/ELU active-idle/ctx-switches) —
123
+ * présente UNIQUEMENT pendant un drill-down de CE worker (Phase 2 : le master a envoyé
124
+ * `nf:probe:enrich` à ce pid). Absente sinon → « on paie ce qu'on regarde » (0 surcoût
125
+ * hors drill). Fusionnée côté front avec `process` pour la vue Supervision complète.
126
+ */
127
+ rich?: IProcessRich;
128
+ /**
129
+ * Santé ORM **lean** du worker (connecteurs/requêtes/erreurs/latence EWMA) — additif,
130
+ * comme `process`. Présent si un driver ORM a branché sa sonde (`setOrmHealthProvider`),
131
+ * absent sinon → les consommateurs realtime l'ignorent (non-breaking). Agrégée pod dans
132
+ * {@link IRealtimeClusterHealth.totals}.`orm`.
133
+ */
134
+ orm?: IOrmLeanHealth;
135
+ /**
136
+ * Compteurs d'erreurs Syslog du worker (ERROR/CRITIC cumulés) — additif. Permet une
137
+ * carte « erreurs par worker » + un taux d'erreur pod (delta côté lecteur). Absent si la
138
+ * sonde n'a pu lire le syslog du kernel.
139
+ */
140
+ errors?: IInstanceErrorHealth;
141
+ /**
142
+ * Drivers de backplane **enregistrés dans ce process** (clés du registre ouvert :
143
+ * natifs + ceux qu'une application a ajoutés par `registerBackplaneDriver`). Sert la
144
+ * même question que la liste des backends côté stores : « qu'est-ce que je POURRAIS
145
+ * brancher ? », à laquelle `backplane.driver` (l'actif) ne répond pas. Absent si le
146
+ * registre n'est pas joignable.
147
+ */
148
+ backplaneDrivers?: string[];
149
+ /**
150
+ * Diagnostic ORM **riche** du worker (par connecteur : ping/latence/stockage/pool +
151
+ * flux requêtes) — blob OPAQUE `{ health, flow }` produit par le driver, présent
152
+ * UNIQUEMENT pendant un drill ORM de CE worker (facette `"orm"` : le master a envoyé
153
+ * `nf:probe:enrich {facet:"orm"}` à ce pid). Absent sinon → « on paie ce qu'on regarde »
154
+ * (0 ping ORM hors drill). Consommé par la page drill `/nodefony/orm/<pid>` (canal
155
+ * `nodefony:orm:rich@<pid>`) → diagnostic riche du pid EXACT (≠ round-robin).
156
+ */
157
+ ormRich?: unknown;
158
+ }
159
+ /**
160
+ * Vue **POD agrégée** de la socket (Phase 4c, mode cluster) — la santé de TOUS les workers
161
+ * du pod consolidée. Servie par n'importe quel worker via l'endpoint santé quand le master
162
+ * pousse le snapshot agrégé (sinon on retombe sur {@link IRealtimeHealth} per-instance).
163
+ *
164
+ * `cluster: true` est le discriminant : un consommateur (Studio) distingue la vue pod de la
165
+ * vue per-instance. `instances` garde le détail par worker (drill-down) ; `totals` somme les
166
+ * scalaires (la backpressure `maxBufferedAmount` est un MAX, pas une somme — c'est le pire pod).
167
+ */
168
+ export interface IRealtimeClusterHealth {
169
+ /** Discriminant : vue agrégée multi-worker (≠ {@link IRealtimeHealth} per-instance). */
170
+ cluster: true;
171
+ /** Horodatage du snapshot agrégé (ms epoch). */
172
+ ts: number;
173
+ /** Nombre de workers présents dans l'agrégat. */
174
+ instanceCount: number;
175
+ /** Santé par worker (détail per-instance pour le drill-down). */
176
+ instances: IRealtimeHealth[];
177
+ /** Totaux pod (sommes des scalaires ; backpressure max = pire worker). */
178
+ totals: {
179
+ channelCount: number;
180
+ publishTotal: number;
181
+ fanoutTotal: number;
182
+ inboundTotal: number;
183
+ /** Ingress backplane refusés, tous workers (signal de sécurité pod-wide). */
184
+ ingressRejectedTotal: number;
185
+ connectionCount: number;
186
+ bytesSentTotal: number;
187
+ messagesSentTotal: number;
188
+ backpressure: {
189
+ /** Pire `maxBufferedAmount` parmi les workers (octets) — pas une somme. */
190
+ maxBufferedAmount: number;
191
+ /** Somme des octets en attente, tous workers. */
192
+ totalBufferedAmount: number;
193
+ /** Total des slow-consumers du pod. */
194
+ slowConsumers: number;
195
+ /** Total des frames jetées par back-pressure (drop + close), tous workers. */
196
+ drops: number;
197
+ };
198
+ /**
199
+ * Santé ORM agrégée du pod (sommes ; `maxEwmaMs` = pire worker). Présent si ≥ 1 worker
200
+ * a remonté sa sonde ORM, absent sinon.
201
+ */
202
+ orm?: IOrmLeanHealth;
203
+ /** Erreurs Syslog agrégées du pod (sommes). Présent si ≥ 1 worker a remonté ses compteurs. */
204
+ errors?: IInstanceErrorHealth;
205
+ };
206
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Jeton realtime — surface MINIMALE de l'identité d'une connexion WebSocket.
3
+ *
4
+ * Sous-ensemble STRICT de `@nodefony/security`'s `IToken` (TypeScript structural
5
+ * typing → toute classe `Token` security l'implémente gratuitement). Le module
6
+ * `@nodefony/realtime` reste ainsi **pur** (zéro dépendance security runtime
7
+ * ni type-only), tout en exposant un contrat partageable.
8
+ *
9
+ * Posé par un {@link IRealtimeAuthenticator} au handshake WS et cacheable côté
10
+ * `RealtimeHub` via la map `peer → token` — les hot-paths (`beforeDispatch`,
11
+ * `onFrameAudit`) le lisent en O(1) sans recalculer l'auth par frame.
12
+ *
13
+ * Zero Trust : un visiteur non authentifié reçoit toujours un token (Anonymous,
14
+ * `isAuthenticated() === false`), jamais `null`.
15
+ *
16
+ * @see IRealtimeAuthenticator
17
+ */
18
+ export interface IRealtimeToken {
19
+ /** Type du token (`"anonymous"`, `"jwt"`, `"apikey"`, …) — discriminator stable. */
20
+ readonly type: string;
21
+ /** Identifiant logique de l'utilisateur (`"anonymous"`, `"user-42"`, …). */
22
+ getUserIdentifier(): string;
23
+ /** `true` si l'authentification a réussi (≠ anonyme). */
24
+ isAuthenticated(): boolean;
25
+ /** Rôles **plats** (sans hiérarchie résolue) — utilisés par les voters P6. */
26
+ getRoles(): string[];
27
+ /**
28
+ * Scopes accordés (clés API / OAuth) — axe **distinct** des rôles RBAC.
29
+ * Ex. `["chat:write", "presence:read"]`. `[]` si aucun.
30
+ */
31
+ getScopes(): string[];
32
+ /** Lecture d'un attribut arbitraire (claims JWT, `tenantId`, providerId…). */
33
+ getAttribute<T = unknown>(key: string): T | undefined;
34
+ /**
35
+ * Re-valide l'identité À L'USAGE (Zero Trust) : `true` si le token est encore
36
+ * légitime, `false` s'il est PÉRIMÉ (session détruite, ou un AUTRE compte s'est
37
+ * connecté depuis sur la même socket partagée). Optionnel — un token qui porte
38
+ * sa propre validité (Anonyme, JWT à `exp`) n'en a pas besoin (**absent =
39
+ * considéré valide**).
40
+ *
41
+ * Appelé par le pont `api.request` AVANT toute action data plane : une
42
+ * WebSocket survit à sa session (le token est figé au handshake), donc une
43
+ * identité périmée ne doit JAMAIS servir une requête data plane sensible.
44
+ * Async (peut relire un store de session) ; gardé HORS du hot-path temps réel
45
+ * (publish/subscribe), payé seulement sur `api.request`.
46
+ */
47
+ isValid?(nowMs?: number): boolean | Promise<boolean>;
48
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Interfaces publiques de @nodefony/realtime.
3
+ *
4
+ * Contrats partagés du module realtime — toutes ces interfaces sont aussi
5
+ * exportées depuis le barrel racine `@nodefony/realtime` pour DX.
6
+ *
7
+ * Sécurité : `IRealtimeToken` / `IRealtimeHandshake` / `IRealtimeAuthenticator`
8
+ * (+ matcher) sont les 3 contrats du **seam sécurité #2** (P13 Bloc A étape 6) —
9
+ * `@nodefony/security` (P6) les implémente sans coupler ce module au security.
10
+ */
11
+ export type { IBackplane, IBackplaneMessage, BackplaneHandler, } from "./IBackplane.js";
12
+ export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./IRealtimeController.js";
13
+ export type { IChannelPolicy } from "./IChannelPolicy.js";
14
+ export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./IRealtimeProbe.js";
15
+ export type { IRealtimeToken } from "./IRealtimeToken.js";
16
+ export type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
17
+ export type { IRealtimeAuthenticator } from "./IRealtimeAuthenticator.js";
18
+ export type { IRealtimeAuthenticatorMatcher, ICompiledRealtimeMatcher, } from "./IRealtimeAuthenticatorMatcher.js";