@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,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;