@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.
- package/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- package/package.json +83 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { JsonRpcPeer, isPlatformChannel } from "nodefony";
|
|
2
|
+
import type { RealtimePublish } from "../../interfaces/IRealtimeController.js";
|
|
3
|
+
import type { IClientLogsLimits } from "../../interfaces/IClientLogsLimits.js";
|
|
4
|
+
import type { IChannelPolicy } from "../../interfaces/IChannelPolicy.js";
|
|
5
|
+
import type { IRealtimeConnProbe, IRealtimeProbe } from "../../interfaces/IRealtimeProbe.js";
|
|
6
|
+
import type { IBackplane } from "../../interfaces/IBackplane.js";
|
|
7
|
+
import type { IRealtimeAuthenticator } from "../../interfaces/IRealtimeAuthenticator.js";
|
|
8
|
+
import type { IRealtimeAuthenticatorMatcher } from "../../interfaces/IRealtimeAuthenticatorMatcher.js";
|
|
9
|
+
import type { IRealtimeHandshake } from "../../interfaces/IRealtimeHandshake.js";
|
|
10
|
+
import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
|
|
11
|
+
/**
|
|
12
|
+
* Fonction de filtrage Origin — `true` = upgrade autorisée. Posée par
|
|
13
|
+
* `RealtimeService.init()` depuis `defineRealtimeConfig().csrf.checkOrigin`.
|
|
14
|
+
* `null` = aucune politique (rétrocompat : tout passe — équivalent
|
|
15
|
+
* `enabled: false`).
|
|
16
|
+
*/
|
|
17
|
+
export type OriginGuard = (origin: string | undefined) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* **Verrou de frame (seam #1 → P6)** — décide si une frame entrante (`api.request`,
|
|
20
|
+
* `subscribe`…) est AUTORISÉE, à partir du {@link IRealtimeToken} déjà résolu au
|
|
21
|
+
* handshake (cold path) et caché sur le peer. `true` = autorisée.
|
|
22
|
+
*
|
|
23
|
+
* Lecture O(1) du token en cache — **JAMAIS de re-authentification ni de lecture
|
|
24
|
+
* base par frame** : l'identité (coûteuse) est figée 1× au handshake, le verrou ne
|
|
25
|
+
* fait que matcher la cible de la frame (path/canal) contre la zone. Posé par
|
|
26
|
+
* `@nodefony/security` au boot via {@link RealtimeService.setFrameAuthorizer}.
|
|
27
|
+
* Doctrine SYNC stricte (cf `JsonRpcPeerOptions.beforeDispatch`) : un `await` par
|
|
28
|
+
* frame coûterait une microtask et sérialiserait le pipeline RPC du peer.
|
|
29
|
+
*/
|
|
30
|
+
export type FrameAuthorizer = (frame: unknown, token: IRealtimeToken) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Seuil d'alerte slow-consumer (octets de `bufferedAmount`). Au-delà, la connexion
|
|
33
|
+
* est comptée comme « lente » par la sonde (PAS encore de drop/close — la sonde MESURE
|
|
34
|
+
* avant qu'on optimise : stringify unique → seuil de drop → coalescing). 1 MiB =
|
|
35
|
+
* `websocket.maxPayload` par défaut : une file qui dépasse une frame max pleine est
|
|
36
|
+
* déjà anormale pour des canaux d'ÉTAT (latest-wins).
|
|
37
|
+
*/
|
|
38
|
+
export declare const SLOW_CONSUMER_BYTES: number;
|
|
39
|
+
/**
|
|
40
|
+
* **Namespace de canaux RÉSERVÉ À LA PLATEFORME** — un seul préfixe, `nodefony:`.
|
|
41
|
+
*
|
|
42
|
+
* Les canaux qu'il couvre exposent l'état interne du pod : journaux, requêtes de
|
|
43
|
+
* base, métriques process, supervision, contrôle du noyau, journal d'audit. Aucun
|
|
44
|
+
* n'appartient au métier d'une application — ils décrivent le serveur lui-même.
|
|
45
|
+
*
|
|
46
|
+
* La liste reste exposée **ici** (au pluriel, sous forme de tableau) parce que le hub
|
|
47
|
+
* est propriétaire de l'espace de nommage des canaux : c'est lui qui les sert.
|
|
48
|
+
* `@nodefony/security`, quand il est chargé, y attache des POLITIQUES (quels rôles)
|
|
49
|
+
* qu'il lit sur cette liste via la surface de service — il ne la redéclare pas. Deux
|
|
50
|
+
* listes auraient divergé au premier namespace ajouté. Le tableau survit à la
|
|
51
|
+
* réduction à une entrée : la forme est un contrat public (`reservedSystemPrefixes()`),
|
|
52
|
+
* et rien n'interdit qu'un jour un second namespace de plateforme apparaisse.
|
|
53
|
+
*
|
|
54
|
+
* Le nom des canaux, lui, vient de la table `PLATFORM_CHANNELS` du cœur (isomorphe) :
|
|
55
|
+
* le navigateur doit connaître les mêmes noms que le serveur.
|
|
56
|
+
*
|
|
57
|
+
* Sans module de sécurité, ces canaux sont **fermés** aux connexions clientes
|
|
58
|
+
* (cf {@link RealtimeHub.subscribeClient}) : aucune identité n'existe alors, donc
|
|
59
|
+
* personne ne peut prouver qu'il a le droit de les lire.
|
|
60
|
+
*/
|
|
61
|
+
export declare const RESERVED_SYSTEM_PREFIXES: readonly ["nodefony:"];
|
|
62
|
+
/**
|
|
63
|
+
* Le canal appartient-il au namespace réservé à la plateforme ?
|
|
64
|
+
*
|
|
65
|
+
* Alias de {@link isPlatformChannel} (cœur isomorphe) sous le nom que porte cette
|
|
66
|
+
* notion côté hub : le hub raisonne en « canal réservé », le client en « surface de
|
|
67
|
+
* plateforme ». Une seule implémentation — la comparaison est insensible à la casse,
|
|
68
|
+
* sinon `NODEFONY:syslog` échapperait au plancher que `nodefony:syslog` subit.
|
|
69
|
+
*
|
|
70
|
+
* @param channel - nom du canal demandé (suffixes de cadence et de forage inclus).
|
|
71
|
+
*/
|
|
72
|
+
export declare const isReservedSystemChannel: typeof isPlatformChannel;
|
|
73
|
+
/**
|
|
74
|
+
* Période de re-validation des identités RÉVOCABLES — F4 (revue 0.6).
|
|
75
|
+
* Le verrou de frame est SYNC par doctrine (identité figée au handshake, cf
|
|
76
|
+
* {@link FrameAuthorizer}) → il ne peut pas re-lire la session par frame. Un socket
|
|
77
|
+
* survivrait donc à sa session (`subscribe` garderait ses flux après un logout HTTP),
|
|
78
|
+
* là où `api.request` re-valide déjà par requête (`isValid()`). Ce tick ferme
|
|
79
|
+
* l'écart : toutes les `REVOCATION_REVALIDATE_MS`, les tokens révocables sont
|
|
80
|
+
* re-validés et la connexion fermée si l'identité n'est plus valide (≤ 1 fenêtre de
|
|
81
|
+
* délai). Ordre de grandeur aligné sur le heartbeat WS (`keepaliveInterval` 20 s).
|
|
82
|
+
*/
|
|
83
|
+
export declare const REVOCATION_REVALIDATE_MS = 30000;
|
|
84
|
+
/**
|
|
85
|
+
* Connexion à identité RÉVOCABLE inscrite au registre de re-validation (F4). Ne
|
|
86
|
+
* porte QUE ce qu'il faut pour re-valider puis fermer : le token (avec `isValid`) et
|
|
87
|
+
* un `close` fermant la socket brute. 1 entrée = 1 connexion à identité révocable
|
|
88
|
+
* (session BFF relue, ou jeton porteur à borne/denylist).
|
|
89
|
+
*/
|
|
90
|
+
export interface IRevocableConnection {
|
|
91
|
+
readonly token: IRealtimeToken;
|
|
92
|
+
readonly close: (code: number, reason: string) => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Sink d'un canal : pousse une charge vers UNE connexion abonnée (son `peer.notify`).
|
|
96
|
+
* 1 sink = 1 connexion. Le hub fan-out la charge à tous les sinks d'un canal.
|
|
97
|
+
*
|
|
98
|
+
* `serialized` n'est fourni que si le canal a déclaré un {@link ChannelSerializer}
|
|
99
|
+
* ET qu'il a plusieurs abonnés : la frame ayant été sérialisée une fois pour tous,
|
|
100
|
+
* le sink peut l'envoyer telle quelle au lieu de refaire le travail. Absent, le
|
|
101
|
+
* sink se débrouille comme avant — un sink historique à un seul paramètre ignore
|
|
102
|
+
* simplement l'argument.
|
|
103
|
+
*/
|
|
104
|
+
export type ChannelSink = (payload: unknown, serialized?: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Sérialiseur de la frame d'un canal, fourni par l'abonné (le hub ignore tout du
|
|
107
|
+
* protocole). Appelé **au plus une fois par publication**, jamais par abonné.
|
|
108
|
+
*/
|
|
109
|
+
export type ChannelSerializer = (payload: unknown) => string;
|
|
110
|
+
/**
|
|
111
|
+
* Fabrique le provider PARTAGÉ d'un canal (listener/ticker qui pousse via `publish`).
|
|
112
|
+
* Appelée UNE fois, au 1ᵉʳ abonné. Renvoie son `dispose` (appelé au dernier
|
|
113
|
+
* désabonné) ou `null` si le canal est inconnu. Le provider doit capturer des deps
|
|
114
|
+
* **long-lived** (kernel/syslog/broker, valeurs simples) — JAMAIS la connexion qui
|
|
115
|
+
* a déclenché la création (elle peut fermer alors que le provider partagé survit).
|
|
116
|
+
*/
|
|
117
|
+
export type ChannelFactory = (channel: string, publish: RealtimePublish) => (() => void) | null;
|
|
118
|
+
/**
|
|
119
|
+
* RealtimeHub — broker temps réel **PAR INSTANCE** (1 pod = 1 process = 1 hub local).
|
|
120
|
+
*
|
|
121
|
+
* Élève le realtime serveur du modèle **per-connexion** (chaque connexion son propre
|
|
122
|
+
* ticker/listener = N fois le même travail) au modèle **canaux PARTAGÉS** : un canal a
|
|
123
|
+
* **UN provider** créé au 1ᵉʳ abonné et un **fan-out** vers tous les abonnés. C'est le
|
|
124
|
+
* gain cloud-native (1 ticker/canal/pod) ET le **seam du backplane Redis** (le cross-pod
|
|
125
|
+
* se branchera dans {@link publish} : fan-out local + forward Redis, l'ingress backplane
|
|
126
|
+
* ne refaisant QUE du fan-out local — règle anti-boucle). Cf vision « la socket Nodefony ».
|
|
127
|
+
*
|
|
128
|
+
* Périmètre actuel : canaux **broadcast** (même flux pour tous les abonnés — stats,
|
|
129
|
+
* syslog, orm…). Les canaux **privés/par-connexion** (ex. une ligne SIP par user) =
|
|
130
|
+
* seam futur (le provider serait per-connexion, pas partagé).
|
|
131
|
+
*
|
|
132
|
+
* Perf (règle ABSOLUE) : map des canaux **lazy** (rien alloué tant qu'aucun abonné) ;
|
|
133
|
+
* provider créé au 1ᵉʳ abonné, **disposé au dernier** (aucun timer/listener orphelin) ;
|
|
134
|
+
* fan-out isolé (une connexion fautive ne casse pas la diffusion). Le hub lui-même est
|
|
135
|
+
* **sans dépendance** : ce sont les *factories* (fournies par les contrôleurs) qui
|
|
136
|
+
* portent les deps.
|
|
137
|
+
*
|
|
138
|
+
* Vocabulaire : la **socket** ({@link IRealtimeSocket}) est la prise que tient le métier ;
|
|
139
|
+
* ce **hub** est le broker serveur caché derrière (registre + fan-out local). La socket
|
|
140
|
+
* multiplexe des canaux ; le hub aiguille entre les sockets. La façade *consommateur*
|
|
141
|
+
* côté serveur EXISTE — {@link ServerRealtimeSocket} (`subscribe/on/publish`), exportée
|
|
142
|
+
* par le barrel du module : c'est elle qu'un service back tient, jamais ce hub
|
|
143
|
+
* directement. Même rôle que `RealtimeClient` côté navigateur.
|
|
144
|
+
*/
|
|
145
|
+
export declare class RealtimeHub {
|
|
146
|
+
#private;
|
|
147
|
+
/**
|
|
148
|
+
* Abonnement demandé par une **connexion cliente** — même chose que
|
|
149
|
+
* {@link subscribe}, plus le plancher des canaux de plateforme.
|
|
150
|
+
*
|
|
151
|
+
* Pourquoi deux portes : un service du serveur qui écoute ses propres journaux
|
|
152
|
+
* est légitime ; une connexion distante qui demande `nodefony:syslog` alors
|
|
153
|
+
* qu'aucun module de sécurité n'est chargé ne l'est pas. Le hub ne peut pas
|
|
154
|
+
* deviner l'origine d'un appel — le contrôleur, lui, sait qu'il traite une
|
|
155
|
+
* frame venue du réseau, et passe donc par ici.
|
|
156
|
+
*
|
|
157
|
+
* Le plancher ne s'applique **que** sans verrou de frame. Dès qu'un module de
|
|
158
|
+
* sécurité en pose un, c'est lui qui décide (avec les rôles, ce que le hub ne
|
|
159
|
+
* sait pas faire) et le plancher s'efface.
|
|
160
|
+
*
|
|
161
|
+
* @returns `true` si abonné, `false` si canal inconnu ou fermé par le plancher.
|
|
162
|
+
*/
|
|
163
|
+
subscribeClient(channel: string, sink: ChannelSink, factory: ChannelFactory, serialize?: ChannelSerializer): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Abonne un consommateur qui **répond du canal** : il apporte la `factory`, donc
|
|
166
|
+
* la réponse à « ce canal existe-t-il, et qui le produit ? ». Le provider partagé
|
|
167
|
+
* est créé au 1ᵉʳ abonné de ce genre. Le sink est inscrit AVANT l'appel à
|
|
168
|
+
* `factory` → le 1ᵉʳ paquet immédiat éventuel du provider (ex. tick initial de
|
|
169
|
+
* `createBrokerTicker`) atteint bien cet abonné.
|
|
170
|
+
*
|
|
171
|
+
* Un canal en état **passif** (ouvert par des écouteurs seuls, cf {@link listen})
|
|
172
|
+
* n'est PAS considéré comme existant : la fabrique est rejouée et tranche. Un
|
|
173
|
+
* refus laisse les écouteurs en place — ils n'ont jamais prétendu le produire.
|
|
174
|
+
*
|
|
175
|
+
* @returns `true` si abonné, `false` si canal inconnu (`factory` a renvoyé `null`).
|
|
176
|
+
*/
|
|
177
|
+
subscribe(channel: string, sink: ChannelSink, factory: ChannelFactory, serialize?: ChannelSerializer): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* **Écoute passive** — un service du serveur reçoit ce qui passe sur un canal
|
|
180
|
+
* sans prétendre le produire. C'est la porte de {@link ServerRealtimeSocket} :
|
|
181
|
+
* un service back qui `subscribe` sur sa façade écoute, il n'ouvre rien.
|
|
182
|
+
*
|
|
183
|
+
* Différence avec {@link subscribe} : aucune fabrique, donc aucun provider et
|
|
184
|
+
* **aucune revendication d'existence**. Si le canal n'a pas encore de
|
|
185
|
+
* propriétaire, l'état créé est marqué passif ; la première connexion cliente
|
|
186
|
+
* qui le demandera fera trancher la fabrique du controller. Un canal déjà
|
|
187
|
+
* ouvert est simplement rejoint (le provider partagé n'est pas touché).
|
|
188
|
+
*
|
|
189
|
+
* Un écouteur n'échoue jamais : il entendra si — et quand — quelqu'un publie.
|
|
190
|
+
* Cold path (abonnement d'un service, pas une frame réseau).
|
|
191
|
+
*/
|
|
192
|
+
listen(channel: string, sink: ChannelSink): void;
|
|
193
|
+
/**
|
|
194
|
+
* Le canal a-t-il un propriétaire (une fabrique a fourni son provider) ?
|
|
195
|
+
* `false` = inconnu, ou tenu par de seuls écouteurs ({@link listen}).
|
|
196
|
+
*/
|
|
197
|
+
isChannelOwned(channel: string): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Désabonne une connexion d'un canal. Au **dernier** abonné, `dispose()` le provider
|
|
200
|
+
* et retire le canal (libère timers/listeners). No-op si non abonné.
|
|
201
|
+
*/
|
|
202
|
+
unsubscribe(channel: string, sink: ChannelSink): void;
|
|
203
|
+
/**
|
|
204
|
+
* Publie une charge sur un canal : **fan-out local** **+** propagation conditionnelle
|
|
205
|
+
* au {@link IBackplane} (autres workers/pods). C'est le point d'entrée des providers
|
|
206
|
+
* locaux (tickers, listeners) ET de tout code serveur.
|
|
207
|
+
*
|
|
208
|
+
* Forward **opt-in par canal** : la charge ne traverse le backplane que si le canal
|
|
209
|
+
* est déclaré **broadcast** ({@link markBroadcastChannel}). Par défaut un canal est
|
|
210
|
+
* **instance-local** (per-instance : observabilité, état du pod) → il reste dans le
|
|
211
|
+
* process. Le flag est mis en cache dans l'état du canal (lu en O(1)) ; pour un
|
|
212
|
+
* `publish` serveur sans abonné local (pas d'état), la politique est évaluée à la volée.
|
|
213
|
+
*
|
|
214
|
+
* Anti-boucle : un message **reçu** du backplane ne repasse PAS par ici (il appelle
|
|
215
|
+
* `publishLocal` directement) → aucune ré-émission cross-process, pas de tempête.
|
|
216
|
+
*/
|
|
217
|
+
publish(channel: string, payload: unknown): void;
|
|
218
|
+
/**
|
|
219
|
+
* Fan-out d'une charge aux **seuls abonnés locaux** d'un canal (process courant).
|
|
220
|
+
* NE propage PAS au backplane → c'est la voie d'**ingress** d'un message venu d'un
|
|
221
|
+
* autre pair (le hub câble `backplane.onMessage` dessus dans {@link setBackplane}) :
|
|
222
|
+
* réinjection locale sans reboucler. Aussi appelable directement pour un push
|
|
223
|
+
* strictement local (jamais propagé).
|
|
224
|
+
*/
|
|
225
|
+
publishLocal(channel: string, payload: unknown): void;
|
|
226
|
+
/**
|
|
227
|
+
* Déclare un **préfixe** de canal **broadcast** : ses publications traversent le
|
|
228
|
+
* {@link IBackplane} (cross-process). Tout le reste demeure instance-local (défaut sûr).
|
|
229
|
+
* Le préfixe couvre la granularité de cadence (`chat:` matche `chat:room1:1000`).
|
|
230
|
+
*
|
|
231
|
+
* Idempotent ; cold-path (appelé au handshake/boot d'un endpoint realtime, jamais
|
|
232
|
+
* sur le chemin chaud). Réévalue les canaux DÉJÀ actifs → l'ordre déclaration/abonnement
|
|
233
|
+
* n'a pas d'importance.
|
|
234
|
+
*
|
|
235
|
+
* @param prefix - préfixe de canal à forwarder (ex. `"chat:"`, `"presence:"`).
|
|
236
|
+
*/
|
|
237
|
+
markBroadcastChannel(prefix: string): void;
|
|
238
|
+
/**
|
|
239
|
+
* Branche le {@link IBackplane} cross-process (cluster IPC, Redis…) et câble son
|
|
240
|
+
* ingress ({@link #admitFromBackplane}) : un message reçu d'un autre pair est
|
|
241
|
+
* réinjecté en **fan-out local uniquement** ({@link publishLocal}) — barrière
|
|
242
|
+
* anti-boucle côté hub (le backplane filtre déjà son propre echo). Remplace un
|
|
243
|
+
* backplane précédent (l'appelant est responsable de `stop()` l'ancien). Démarre
|
|
244
|
+
* le transport.
|
|
245
|
+
*
|
|
246
|
+
* @returns le backplane branché (chaînage).
|
|
247
|
+
*/
|
|
248
|
+
setBackplane(backplane: IBackplane): IBackplane;
|
|
249
|
+
/** Backplane cross-process branché, ou `null` en mono-process (lecture/tests/sonde). */
|
|
250
|
+
get backplane(): IBackplane | null;
|
|
251
|
+
/** Nombre d'abonnés locaux d'un canal (observabilité / tests). */
|
|
252
|
+
subscriberCount(channel: string): number;
|
|
253
|
+
/**
|
|
254
|
+
* Inscrit une connexion au registre de la sonde (au handshake). Lazy : alloue le
|
|
255
|
+
* Set au 1ᵉʳ appel. Le hub ne lit ces connexions QUE dans {@link probe} (jamais
|
|
256
|
+
* sur le chemin chaud). À équilibrer par {@link unregisterConnection} au close.
|
|
257
|
+
*/
|
|
258
|
+
registerConnection(conn: IRealtimeConnProbe): void;
|
|
259
|
+
/** Retire une connexion du registre de la sonde (au close). No-op si absente. */
|
|
260
|
+
unregisterConnection(conn: IRealtimeConnProbe): void;
|
|
261
|
+
/**
|
|
262
|
+
* Inscrit une connexion à identité RÉVOCABLE au registre de
|
|
263
|
+
* re-validation (F4, revue 0.6). Démarre le tick au 1ᵉʳ inscrit (timer `unref`,
|
|
264
|
+
* 0 timer au repos). À équilibrer par {@link unregisterRevocable} au close.
|
|
265
|
+
* N'inscrire QUE les tokens portant `isValid` (le controller filtre) : une identité
|
|
266
|
+
* non révocable (anonyme/JWT sans revalidation) ne doit pas y entrer.
|
|
267
|
+
*/
|
|
268
|
+
registerRevocable(entry: IRevocableConnection): void;
|
|
269
|
+
/**
|
|
270
|
+
* Retire une connexion du registre de révocation (au close). Arrête le tick dès que
|
|
271
|
+
* le registre est vide (0 timer au repos). No-op si absente.
|
|
272
|
+
*/
|
|
273
|
+
unregisterRevocable(entry: IRevocableConnection): void;
|
|
274
|
+
/**
|
|
275
|
+
* Tick de re-validation (F4) : re-lit l'identité de chaque connexion révocable
|
|
276
|
+
* (`token.isValid()`) et FERME la socket (`4001`) si l'identité est morte : session
|
|
277
|
+
* détruite ou passée à un autre compte, jeton expiré ou révoqué.
|
|
278
|
+
* Fail-closed : une re-validation qui throw ferme aussi (parité `invokeApiRequest`).
|
|
279
|
+
* Snapshot du registre AVANT les `await` (le `close` mute le registre via le cleanup
|
|
280
|
+
* `onFinish`). Exposé (public) pour un test déterministe sans fake timers.
|
|
281
|
+
*/
|
|
282
|
+
revalidateRevocable(): Promise<void>;
|
|
283
|
+
/**
|
|
284
|
+
* Compte une frame entrante full-duplex (canaux gated SIP/bridge). Appelé par le
|
|
285
|
+
* contrôleur quand un handler `realtimeInbound` traite un message client.
|
|
286
|
+
*/
|
|
287
|
+
recordInbound(): void;
|
|
288
|
+
/**
|
|
289
|
+
* Snapshot d'auto-observabilité de la socket (per-instance). Lecture PURE (aucune
|
|
290
|
+
* alloc sur le chemin chaud, jamais throw) : agrège canaux + fan-out + connexions +
|
|
291
|
+
* **backpressure** (`bufferedAmount`, risque #1). Appelé à la demande (endpoint HTTP)
|
|
292
|
+
* ou par le ticker hub `nodefony:socket`. Les cumuls sont monotones → débit dérivé
|
|
293
|
+
* côté lecteur. Cf {@link IRealtimeProbe}.
|
|
294
|
+
*/
|
|
295
|
+
probe(): IRealtimeProbe;
|
|
296
|
+
/** Canaux actifs (≥ 1 abonné). Lecture seule. */
|
|
297
|
+
get activeChannels(): string[];
|
|
298
|
+
/**
|
|
299
|
+
* **Seam #2 (P13 → P6)** — enregistre un authenticator pour les handshakes
|
|
300
|
+
* WS dont l'URL matche `matcher`. Ordre d'enregistrement préservé : le
|
|
301
|
+
* **1ʳᵉ** matcher qui capture la connexion gagne. Convention : enregistrer
|
|
302
|
+
* les patterns les plus spécifiques en premier.
|
|
303
|
+
*
|
|
304
|
+
* Idempotent par couple `(matcher, authenticator)` — appel multiple = même
|
|
305
|
+
* effet (push si pas déjà présent). Cold-path (boot via P6/userland).
|
|
306
|
+
*
|
|
307
|
+
* @param matcher sélecteur (pattern URL + vhost optionnel)
|
|
308
|
+
* @param authenticator stratégie d'auth réseau (handshake → IRealtimeToken)
|
|
309
|
+
*/
|
|
310
|
+
useAuthenticator(matcher: IRealtimeAuthenticatorMatcher, authenticator: IRealtimeAuthenticator): void;
|
|
311
|
+
/**
|
|
312
|
+
* Résout l'authenticator capturant ce handshake (1ʳᵉ matcher qui matche),
|
|
313
|
+
* ou `null` si aucun (le RealtimeController posera un token anonyme).
|
|
314
|
+
* Cold path (1× par upgrade) — lecture array.
|
|
315
|
+
*/
|
|
316
|
+
resolveAuthenticator(handshake: IRealtimeHandshake): IRealtimeAuthenticator | null;
|
|
317
|
+
/**
|
|
318
|
+
* **Seam #4 (P13 → P6)** — pose la politique Origin (RFC 6455 §10.2).
|
|
319
|
+
* `null` = aucune politique (toutes origines acceptées). Posée 1× par
|
|
320
|
+
* `RealtimeService.init()` depuis `defineRealtimeConfig().csrf.checkOrigin`.
|
|
321
|
+
*/
|
|
322
|
+
setOriginGuard(guard: OriginGuard | null): void;
|
|
323
|
+
/**
|
|
324
|
+
* Vérifie qu'une origin est autorisée (politique CSRF). `true` si aucune
|
|
325
|
+
* politique posée (rétrocompat). Cold path (1× par upgrade).
|
|
326
|
+
*/
|
|
327
|
+
checkOrigin(origin: string | undefined): boolean;
|
|
328
|
+
/**
|
|
329
|
+
* F6a (revue 0.6) — plafond de canaux par connexion (anti-OOM). Posé 1× au
|
|
330
|
+
* boot par `RealtimeService.init()` depuis
|
|
331
|
+
* `defineRealtimeConfig().limits.maxChannelsPerConnection`. `null` = illimité.
|
|
332
|
+
*/
|
|
333
|
+
setMaxChannelsPerConnection(max: number | null): void;
|
|
334
|
+
/** Plafond courant de canaux par connexion (`null` = illimité). Lu par `startChannel`. */
|
|
335
|
+
get maxChannelsPerConnection(): number | null;
|
|
336
|
+
/**
|
|
337
|
+
* F9 (revue 0.6) — pose le seuil de comptage des consommateurs lents (`probe()`).
|
|
338
|
+
* Posé 1× au boot par `RealtimeService.init()` depuis
|
|
339
|
+
* `defineRealtimeConfig().slowConsumer.bytes` (sinon la clé de config serait morte).
|
|
340
|
+
*/
|
|
341
|
+
setSlowConsumerBytes(bytes: number): void;
|
|
342
|
+
/**
|
|
343
|
+
* Associe un token au peer (posé par `RealtimeController.onHandshake()` après
|
|
344
|
+
* succès `authenticator.authenticate()` ou fallback anonyme). Lazy alloc de
|
|
345
|
+
* la `WeakMap` au 1ᵉʳ appel (0 coût si aucun authenticator enregistré).
|
|
346
|
+
*/
|
|
347
|
+
setTokenForPeer(peer: JsonRpcPeer, token: IRealtimeToken): void;
|
|
348
|
+
/**
|
|
349
|
+
* Renvoie le token associé au peer, ou `ANONYMOUS_REALTIME_TOKEN` si aucun
|
|
350
|
+
* (cas Zero Trust : la lecture ne renvoie JAMAIS `null` — slot #6 audit
|
|
351
|
+
* lookup garanti, voters P6 ne se posent jamais la question « actor ? »).
|
|
352
|
+
*/
|
|
353
|
+
getTokenForPeer(peer: JsonRpcPeer): IRealtimeToken;
|
|
354
|
+
/**
|
|
355
|
+
* **Seam #1 (P13 → P6)** — pose le verrou de frame (cf {@link FrameAuthorizer}).
|
|
356
|
+
* `null` retire la politique (bypass total). Posé 1× au boot par
|
|
357
|
+
* `@nodefony/security` depuis les zones `realtime: true`. Cold path.
|
|
358
|
+
*/
|
|
359
|
+
setFrameAuthorizer(authorizer: FrameAuthorizer | null, options?: {
|
|
360
|
+
readonly silentProbe?: FrameAuthorizer | null;
|
|
361
|
+
}): void;
|
|
362
|
+
/**
|
|
363
|
+
* Un verrou de frame est-il posé ?
|
|
364
|
+
*
|
|
365
|
+
* Sert au DIAGNOSTIC (`hasUnenforcedChannelPolicies`, introspection, tests) —
|
|
366
|
+
* **plus** à décider si on arme le verrou sur une connexion : le contrôleur
|
|
367
|
+
* l'arme toujours, précisément pour qu'une politique posée après coup garde
|
|
368
|
+
* les sockets déjà ouvertes.
|
|
369
|
+
*/
|
|
370
|
+
hasFrameAuthorizer(): boolean;
|
|
371
|
+
/**
|
|
372
|
+
* Branche l'alerte du plancher système — tirée **une seule fois**, au premier
|
|
373
|
+
* canal de plateforme refusé faute de module de sécurité.
|
|
374
|
+
*
|
|
375
|
+
* Posée par le contrôleur : le hub est sans dépendance (aucun journal), et une
|
|
376
|
+
* fermeture silencieuse serait exactement la dégradation muette qu'on cherche
|
|
377
|
+
* à interdire.
|
|
378
|
+
*
|
|
379
|
+
* @param notify - reçoit le message à journaliser (niveau avertissement).
|
|
380
|
+
*/
|
|
381
|
+
onPlatformNotice(notify: (message: string, severity: "WARNING" | "INFO") => void): void;
|
|
382
|
+
/**
|
|
383
|
+
* Ce canal serait-il fermé par le plancher système ? Lecture PURE (ni compteur,
|
|
384
|
+
* ni alerte) — elle sert à expliquer un refus au client plutôt qu'à le décider.
|
|
385
|
+
*
|
|
386
|
+
* Sans elle, un abonnement refusé resterait sans réponse : le client attendrait
|
|
387
|
+
* indéfiniment des données qui ne viendront pas, ce qui est exactement la
|
|
388
|
+
* dégradation muette que ce plancher est censé faire disparaître.
|
|
389
|
+
*/
|
|
390
|
+
isClosedBySystemFloor(channel: string): boolean;
|
|
391
|
+
/**
|
|
392
|
+
* F1 (revue 0.6) — des politiques de canal sont-elles DÉCLARÉES (`@RealtimeChannel`
|
|
393
|
+
* avec `roles`/`scopes`/`authenticated`) sans `frameAuthorizer` pour les faire
|
|
394
|
+
* respecter ? `true` = **dégradation silencieuse** : un canal se croit gardé mais
|
|
395
|
+
* aucun décideur n'est câblé → un subscribe passe non gardé. Vrai uniquement sans
|
|
396
|
+
* `@nodefony/security` (ou toutes les zones en `realtime: false`). Lu par le
|
|
397
|
+
* RealtimeController pour émettre un WARNING au boot (fail-loud, jamais silencieux).
|
|
398
|
+
*/
|
|
399
|
+
hasUnenforcedChannelPolicies(): boolean;
|
|
400
|
+
/**
|
|
401
|
+
* **Seam #1b** — déclare la politique d'autorisation d'un canal/méthode
|
|
402
|
+
* (`@RealtimeChannel`/`@RealtimeInbound` avec opts). Appelé au handshake par
|
|
403
|
+
* le controller pour CHAQUE canal décoré (idempotent : même nom = écrase, les
|
|
404
|
+
* controllers d'un même endpoint déclarent la même policy). Cold path.
|
|
405
|
+
*
|
|
406
|
+
* Deux formes de nom, et la seconde garde une FAMILLE :
|
|
407
|
+
*
|
|
408
|
+
* - un nom **exact** (`chat:room`) — lecture O(1) au `subscribe` ;
|
|
409
|
+
* - un **motif** (`chat:room:*`) — il couvre tout canal dérivé servi par une
|
|
410
|
+
* fabrique dynamique (`chat:room:1000`), ce qu'un nom exact ne faisait pas.
|
|
411
|
+
* Le `*` remplace n'importe quelle suite de caractères ; tout le reste du
|
|
412
|
+
* motif est littéral (échappé), y compris `.` et `(`.
|
|
413
|
+
*
|
|
414
|
+
* Précédence : le nom **exact** l'emporte toujours — c'est ainsi qu'on ouvre
|
|
415
|
+
* une exception dans une famille gardée. Entre deux motifs, le plus
|
|
416
|
+
* **spécifique** gagne (littéral le plus long).
|
|
417
|
+
*
|
|
418
|
+
* @param name - nom exact du canal (subscribe), de la méthode inbound, ou
|
|
419
|
+
* MOTIF de famille s'il contient `*`.
|
|
420
|
+
* @param policy - exigences ({@link IChannelPolicy}) lues par security.
|
|
421
|
+
*/
|
|
422
|
+
registerChannelPolicy(name: string, policy: IChannelPolicy): void;
|
|
423
|
+
/**
|
|
424
|
+
* Nombre maximum de `*` dans un motif.
|
|
425
|
+
*
|
|
426
|
+
* Ce n'est pas une limite de confort : chaque `*` devient un `.*` dans
|
|
427
|
+
* l'expression compilée, et plusieurs `.*` sur une chaîne qui NE matche pas
|
|
428
|
+
* font revenir le moteur sur ses pas — un coût qui explose avec leur nombre.
|
|
429
|
+
* Un motif de canal en demande un, deux au pire ; au-delà, c'est une erreur
|
|
430
|
+
* de déclaration, et on le DIT plutôt que d'exposer un chemin coûteux à ce
|
|
431
|
+
* qu'un client peut nommer.
|
|
432
|
+
*/
|
|
433
|
+
static readonly MAX_PATTERN_WILDCARDS = 3;
|
|
434
|
+
/**
|
|
435
|
+
* Politique déclarée pour ce canal/méthode, ou `null` si aucune. Lu par
|
|
436
|
+
* `@nodefony/security` (`resolveChannelPolicy` de la surface DI) au moment de
|
|
437
|
+
* la frame `subscribe`/inbound. O(1), 0 alloc quand le registre est vide.
|
|
438
|
+
*/
|
|
439
|
+
resolveChannelPolicy(name: string): IChannelPolicy | null;
|
|
440
|
+
/** Des politiques de canal sont-elles déclarées (au moins une) ? */
|
|
441
|
+
hasChannelPolicies(): boolean;
|
|
442
|
+
/**
|
|
443
|
+
* Avertit — **une seule fois** — qu'un canal servi par une fabrique
|
|
444
|
+
* DYNAMIQUE n'est couvert par aucune politique déclarée, alors que d'autres
|
|
445
|
+
* canaux du module en portent une.
|
|
446
|
+
*
|
|
447
|
+
* Le registre des politiques est indexé par nom **EXACT** : `@RealtimeChannel`
|
|
448
|
+
* déclare `"chat:room"`, mais un canal dérivé (`chat:room:1000` — cadence,
|
|
449
|
+
* forage, suffixe) est servi par la fabrique dynamique et **ne matche pas**.
|
|
450
|
+
* L'auteur croit alors son canal gardé ; il ne l'est pas, et rien ne le lui
|
|
451
|
+
* dit. C'est ce silence qu'on ferme ici — et depuis qu'une politique peut se
|
|
452
|
+
* déclarer par MOTIF (`chat:room:*`), cet avertissement ne parle plus que des
|
|
453
|
+
* canaux qu'aucun motif ne couvre : il désigne un vrai trou, pas une limite
|
|
454
|
+
* de l'outil.
|
|
455
|
+
*
|
|
456
|
+
* Appelé par le contrôleur, seul à savoir par quelle voie le canal a été
|
|
457
|
+
* servi. Muet si aucune politique n'est déclarée (module aux canaux libres :
|
|
458
|
+
* il n'y a alors aucune promesse à trahir).
|
|
459
|
+
*
|
|
460
|
+
* @param channel - le canal servi dynamiquement, cité en exemple dans l'alerte.
|
|
461
|
+
*/
|
|
462
|
+
noticeUnguardedDynamicChannel(channel: string): void;
|
|
463
|
+
/**
|
|
464
|
+
* Enregistre la **factory d'un canal système** (plateforme) — un module bas
|
|
465
|
+
* niveau (ex. `@nodefony/security` : `nodefony:audit`) déclare au boot comment
|
|
466
|
+
* produire le provider d'un canal, SANS qu'aucun `RealtimeController` ne le
|
|
467
|
+
* connaisse. `subscribe` consulte ce registre quand la factory du controller
|
|
468
|
+
* renvoie `null` → le canal devient servable par TOUT endpoint (présent/futur),
|
|
469
|
+
* zéro couplage à Studio. La factory garde la sémantique lazy du hub (créée au
|
|
470
|
+
* 1ᵉʳ abonné, `dispose` au dernier).
|
|
471
|
+
*
|
|
472
|
+
* @param channel - nom EXACT du canal système (match exact, pas un préfixe).
|
|
473
|
+
* @param factory - {@link ChannelFactory} qui crée le provider partagé.
|
|
474
|
+
*/
|
|
475
|
+
registerSystemChannel(channel: string, factory: ChannelFactory): void;
|
|
476
|
+
/**
|
|
477
|
+
* Pont hot-path entre `JsonRpcPeer.beforeDispatch` et le verrou métier : lit
|
|
478
|
+
* le token déjà caché du peer (O(1), 0 lecture base) puis délègue à
|
|
479
|
+
* l'authorizer. `true` (autorisée) si aucun verrou posé (bypass 0-coût) —
|
|
480
|
+
* mais en pratique `beforeDispatch` n'est branché que si `hasFrameAuthorizer()`.
|
|
481
|
+
*
|
|
482
|
+
* @param frame - frame JSON-RPC entrante (brute) à autoriser.
|
|
483
|
+
* @param peer - peer émetteur (clé du mapping `peer → token`).
|
|
484
|
+
* @returns `true` si la frame peut être dispatchée, `false` si refusée.
|
|
485
|
+
*/
|
|
486
|
+
/**
|
|
487
|
+
* Ouvre (ou referme) la réception des journaux du navigateur, avec ses bornes.
|
|
488
|
+
*
|
|
489
|
+
* @param limits - bornes de débit et de taille, ou `null` pour refermer le canal.
|
|
490
|
+
*/
|
|
491
|
+
setClientLogsLimits(limits: IClientLogsLimits | null): void;
|
|
492
|
+
/** Bornes en vigueur, ou `null` si le canal montant est fermé. */
|
|
493
|
+
get clientLogsLimits(): IClientLogsLimits | null;
|
|
494
|
+
runAuthorizer(frame: unknown, peer: JsonRpcPeer): boolean;
|
|
495
|
+
/**
|
|
496
|
+
* Ce peer pourrait-il s'abonner à ce canal ? — même décision que `subscribe`,
|
|
497
|
+
* sans trace ni effet de bord.
|
|
498
|
+
*
|
|
499
|
+
* Rend `true` quand aucune sonde n'est posée : sans sécurité chargée, aucun
|
|
500
|
+
* canal n'est refusé, et l'absence de sonde ne doit jamais faire DISPARAÎTRE
|
|
501
|
+
* des canaux d'un welcome. Un filtre qui échoue doit échouer ouvert ici —
|
|
502
|
+
* c'est le verrou, pas l'annonce, qui protège l'accès.
|
|
503
|
+
*
|
|
504
|
+
* @param channel - le canal interrogé.
|
|
505
|
+
* @param peer - la connexion, d'où l'on tire le jeton résolu au handshake.
|
|
506
|
+
* @returns `true` si un `subscribe` sur ce canal serait accepté.
|
|
507
|
+
*/
|
|
508
|
+
probeChannel(channel: string, peer: JsonRpcPeer): boolean;
|
|
509
|
+
/** Authenticators enregistrés (debug / tests). Lecture seule. */
|
|
510
|
+
get registeredAuthenticators(): ReadonlyArray<IRealtimeAuthenticator>;
|
|
511
|
+
/** Dispose tous les providers et vide le hub (arrêt process / reset de test). */
|
|
512
|
+
clear(): void;
|
|
513
|
+
}
|
|
514
|
+
/** Renvoie le hub realtime du process (singleton lazy). */
|
|
515
|
+
export declare function getRealtimeHub(): RealtimeHub;
|
|
516
|
+
export default RealtimeHub;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type IRealtimeSocket, type IRealtimeChannel, type IChannelStats, type RealtimeHandler, type EventsMap, type ActionsMap, type DefaultEventsMap, type DefaultActionsMap, type EventNames, type EventPayload, type ContractParams, type ContractResult } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **L4 — façade serveur « la socket Nodefony ».** Un handle SERVEUR qui implémente
|
|
4
|
+
* le contrat ISOMORPHE {@link IRealtimeSocket} au-dessus du {@link RealtimeHub} : un
|
|
5
|
+
* service back code **comme une page front** (`publish` / `subscribe` / `on` /
|
|
6
|
+
* `channel`) — il tient UN handle, ignore le fan-out et le backplane.
|
|
7
|
+
*
|
|
8
|
+
* Tient la promesse North Star « un consommateur — front OU back — tient UN handle ».
|
|
9
|
+
*
|
|
10
|
+
* ── Sémantique côté serveur ──
|
|
11
|
+
* - `publish(channel, payload)` : **fan-out** aux abonnés via le hub (+ backplane si
|
|
12
|
+
* le canal est broadcast). C'est le cas d'usage principal (un service pousse des
|
|
13
|
+
* events que les clients reçoivent).
|
|
14
|
+
* - `subscribe`/`on` : le service ÉCOUTE un canal (sink interne) — **écoute passive**
|
|
15
|
+
* (`RealtimeHub.listen`). Écouter n'ouvre PAS le canal : s'il n'a pas de provider,
|
|
16
|
+
* l'état créé est passif, et c'est la fabrique du controller qui tranchera quand
|
|
17
|
+
* une connexion cliente le demandera. Un service n'invente donc jamais un canal
|
|
18
|
+
* pour les autres.
|
|
19
|
+
* - `request` : **non supporté** — un handle au-dessus du hub n'a PAS de pair unique
|
|
20
|
+
* (le hub est multi-clients). Pour un RPC serveur→client 1-1, utiliser
|
|
21
|
+
* `RealtimeController.requestClient` (par connexion, L1).
|
|
22
|
+
*
|
|
23
|
+
* Lazy : aucune structure allouée tant que le service n'`on`/`subscribe`/`publish` pas.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam Emit — canaux pub/sub SORTANTS (`publish`) = ServerToClient.
|
|
26
|
+
* @typeParam Listen — canaux pub/sub RÉCEPTIONNÉS (`subscribe`/`on`) = ClientToServer.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> implements IRealtimeSocket<Emit, Listen, Actions> {
|
|
29
|
+
#private;
|
|
30
|
+
private readonly hub;
|
|
31
|
+
constructor(hub?: import("./RealtimeHub.js").RealtimeHub);
|
|
32
|
+
/** Émet sur un canal — fan-out aux abonnés via le hub (+ backplane si broadcast). */
|
|
33
|
+
publish<K extends string>(channel: K, payload?: K extends EventNames<Emit> ? EventPayload<Emit, K> : unknown): void;
|
|
34
|
+
/**
|
|
35
|
+
* S'abonne (ref-compté) à un canal pour le RECEVOIR. Au 1ᵉʳ consommateur, pose un
|
|
36
|
+
* sink d'**écoute passive** auprès du hub : le service reçoit ce qui passe, sans
|
|
37
|
+
* ouvrir le canal à qui que ce soit d'autre.
|
|
38
|
+
*/
|
|
39
|
+
subscribe(channel: EventNames<Listen> | (string & {})): void;
|
|
40
|
+
/** Désabonne (ref-compté) : coupe le sink du hub au DERNIER consommateur. */
|
|
41
|
+
unsubscribe(channel: EventNames<Listen> | (string & {})): void;
|
|
42
|
+
/** Branche un handler de réception sur un canal. Renvoie un `dispose`. */
|
|
43
|
+
on<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): () => void;
|
|
44
|
+
/** Retire un handler d'un canal. */
|
|
45
|
+
off<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): void;
|
|
46
|
+
/**
|
|
47
|
+
* **Non supporté.** Un handle au-dessus du hub n'a pas de pair unique
|
|
48
|
+
* (multi-clients). Pour un RPC serveur→client 1-1, utiliser
|
|
49
|
+
* `RealtimeController.requestClient` (par connexion, L1).
|
|
50
|
+
*/
|
|
51
|
+
request<K extends string, T = unknown>(_method: K, _params?: ContractParams<Actions, K>, _timeoutMs?: number): Promise<ContractResult<Actions, K, T>>;
|
|
52
|
+
/** Vue par-canal ({@link IRealtimeChannel}) — fine liaison sur les primitives. */
|
|
53
|
+
channel(name: string): IRealtimeChannel;
|
|
54
|
+
/** Snapshot des compteurs par canal (refs internes — à LIRE). */
|
|
55
|
+
getStats(): IChannelStats[];
|
|
56
|
+
/** Compteurs d'un canal précis ou `undefined`. */
|
|
57
|
+
getChannelStats(channel: string): IChannelStats | undefined;
|
|
58
|
+
/** Canaux actuellement abonnés (≥ 1 consommateur). */
|
|
59
|
+
get subscribedChannels(): string[];
|
|
60
|
+
}
|
|
61
|
+
/** Crée un handle serveur ({@link ServerRealtimeSocket}) sur le hub courant. */
|
|
62
|
+
export declare function serverSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap>(): ServerRealtimeSocket<Emit, Listen, Actions>;
|
|
63
|
+
export default ServerRealtimeSocket;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le détail d'un `realtime:denied` — dit au développeur, tu à l'utilisateur.
|
|
3
|
+
*
|
|
4
|
+
* `RealtimeDeniedReason` est fermé et générique par construction : `forbidden`
|
|
5
|
+
* ne dit pas si c'est un rôle qui manque ou le plancher de plateforme qui est
|
|
6
|
+
* clos, et c'est ce qui l'empêche d'être un oracle. Le prix de cette prudence
|
|
7
|
+
* est payé par le développeur : il reçoit un refus qu'il ne sait pas lire, et
|
|
8
|
+
* il lui reste à deviner ce qu'il faut regarder.
|
|
9
|
+
*
|
|
10
|
+
* Cette fonction pose donc la phrase utile — **et seulement hors production**,
|
|
11
|
+
* où la même phrase serait exactement l'oracle que `reason` refuse d'être.
|
|
12
|
+
*
|
|
13
|
+
* Isolée pour la même raison que {@link welcomeEnv}, dont elle copie la règle :
|
|
14
|
+
* noyée dans la composition du refus, son inversion ne casserait aucun test et
|
|
15
|
+
* livrerait le détail d'une politique aux visiteurs d'un serveur publié. Une
|
|
16
|
+
* absence vaut production, jamais l'inverse.
|
|
17
|
+
*
|
|
18
|
+
* @param env - le mode du kernel serveur (`kernel.environment`), s'il est connu.
|
|
19
|
+
* @param detail - la phrase à dire, déjà rédigée par l'appelant.
|
|
20
|
+
* @returns le fragment à étaler dans le refus : `{ detail }` hors production,
|
|
21
|
+
* sinon un objet vide.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deniedDetail(env: string | undefined | null, detail: string): {
|
|
24
|
+
detail?: string;
|
|
25
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Réception des journaux **remontés par un navigateur** : le pendant serveur du
|
|
3
|
+
* transport client (`nodefony/client` → `installSyslogUplink`).
|
|
4
|
+
*
|
|
5
|
+
* Un canal entrant qui écrit dans le journal du pod est une **surface d'écriture**, et
|
|
6
|
+
* c'est la seule chose qui compte pour sa conception. Trois bornes, non négociables :
|
|
7
|
+
*
|
|
8
|
+
* 1. **L'origine est FORCÉE.** Le `moduleName` que le client prétend n'est jamais repris :
|
|
9
|
+
* le `Pdu` réinjecté porte {@link BROWSER_ORIGIN}. Sans quoi une page pourrait se faire
|
|
10
|
+
* passer pour le noyau et fabriquer des pistes de diagnostic.
|
|
11
|
+
* 2. **La taille et le débit sont bornés**, par connexion. Un onglet ouvert ne doit pas
|
|
12
|
+
* pouvoir noyer le journal d'exploitation — ni par un lot géant, ni par la répétition.
|
|
13
|
+
* 3. **La sévérité est plafonnée.** Un client ne déclenche pas une alerte d'exploitation :
|
|
14
|
+
* tout ce qu'il envoie de plus grave que {@link MAX_CLIENT_SEVERITY} y est ramené.
|
|
15
|
+
*
|
|
16
|
+
* Ce que ces bornes ne couvrent PAS, et qu'il faut savoir en lisant le journal : le
|
|
17
|
+
* `requestId` d'une entrée est **déclaré par le client**. On le valide en forme, jamais en
|
|
18
|
+
* fond — un client peut donc rattacher ses lignes à une requête qui n'est pas la sienne.
|
|
19
|
+
* L'impact est une piste de diagnostic polluée, jamais une élévation de droit ; et le
|
|
20
|
+
* `moduleName` forcé dit au lecteur que la ligne vient d'un navigateur, donc qu'elle est
|
|
21
|
+
* déclarative. C'est le prix d'une corrélation front↔back, et il s'énonce.
|
|
22
|
+
*/
|
|
23
|
+
import { BROWSER_ORIGIN, Syslog } from "nodefony";
|
|
24
|
+
import type { RealtimeInboundHandler } from "../../interfaces/IRealtimeController.js";
|
|
25
|
+
export { BROWSER_ORIGIN };
|
|
26
|
+
/**
|
|
27
|
+
* Sévérité la plus grave qu'un client puisse obtenir (3 = ERROR). Au-dessus vivent
|
|
28
|
+
* CRITIC, ALERT et EMERGENCY — des niveaux d'exploitation qu'un onglet ne décide pas.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MAX_CLIENT_SEVERITY = 3;
|
|
31
|
+
/** Les seules sévérités qu'une entrée cliente peut porter, une fois plafonnée. */
|
|
32
|
+
export type ClientSeverity = 3 | 4 | 5 | 6 | 7;
|
|
33
|
+
export interface SyslogUplinkHandlerOptions {
|
|
34
|
+
/** Le journal du pod où réinjecter. */
|
|
35
|
+
syslog: Syslog;
|
|
36
|
+
/** Entrées retenues par lot. Défaut `50` ; le surplus est ignoré et compté. */
|
|
37
|
+
maxEntriesPerBatch?: number;
|
|
38
|
+
/** Entrées retenues par fenêtre et par connexion. Défaut `300`. */
|
|
39
|
+
maxEntriesPerWindow?: number;
|
|
40
|
+
/** Durée de la fenêtre de débit, en ms. Défaut `10000`. */
|
|
41
|
+
windowMs?: number;
|
|
42
|
+
/** Longueur maximale d'une chaîne acceptée. Défaut `4096`. */
|
|
43
|
+
maxStringLength?: number;
|
|
44
|
+
/** Longueur maximale d'un `requestId` accepté. Défaut `128`. */
|
|
45
|
+
maxRequestIdLength?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fabrique le handler du canal montant des journaux.
|
|
49
|
+
*
|
|
50
|
+
* **Un handler par connexion** : `RealtimeController.realtimeInbound()` est invoqué au
|
|
51
|
+
* handshake, donc le compteur de débit qu'il capture est per-connexion — ce qui est le
|
|
52
|
+
* seul découpage utile (borner globalement laisserait un onglet bavard museler tous les
|
|
53
|
+
* autres).
|
|
54
|
+
*
|
|
55
|
+
* @param opts - journal cible et bornes.
|
|
56
|
+
* @returns le handler à déclarer dans `realtimeInbound()`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createSyslogUplinkHandler(opts: SyslogUplinkHandlerOptions): RealtimeInboundHandler;
|