@nodefony/realtime 10.0.0-alpha.4 → 10.0.0-alpha.6
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 +201 -543
- package/README.md +1 -1
- package/dist/nodefony/src/server/RealtimeController.js +3 -2
- package/docs/actions.md +2 -2
- package/docs/architecture.md +17 -17
- package/docs/configuration.md +2 -2
- package/docs/cookbook-chat.md +7 -7
- package/docs/observabilite.md +1 -1
- package/docs/protocole.md +1 -1
- package/docs/securite.md +26 -26
- package/docs/vocabulaire.md +27 -27
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ import { createSyslogUplinkHandler } from "./syslogUplink.js";
|
|
|
5
5
|
import { WsConnectionTransport } from "../transport/WsConnectionTransport.js";
|
|
6
6
|
import { welcomeEnv } from "./welcomeEnv.js";
|
|
7
7
|
import { deniedDetail } from "./deniedDetail.js";
|
|
8
|
-
import { JsonRpcPeer, PLATFORM_INBOUND, RequestContext, RpcEnvelope, RpcError } from "nodefony";
|
|
8
|
+
import { JsonRpcPeer, PLATFORM_INBOUND, RequestContext, RpcEnvelope, RpcError, identityHint } from "nodefony";
|
|
9
9
|
import { readBackpressureOptions } from "@nodefony/http";
|
|
10
10
|
import { Controller } from "@nodefony/framework";
|
|
11
11
|
//#region nodefony/src/server/RealtimeController.ts
|
|
@@ -269,10 +269,11 @@ var RealtimeController = class extends Controller {
|
|
|
269
269
|
if (f.id !== void 0) return;
|
|
270
270
|
const channel = f.method === "subscribe" ? typeof f.params?.channel === "string" ? f.params.channel : void 0 : typeof f.method === "string" ? f.method : void 0;
|
|
271
271
|
if (channel !== void 0) {
|
|
272
|
+
const hint = identityHint(this.kernel);
|
|
272
273
|
const denied = {
|
|
273
274
|
channel,
|
|
274
275
|
reason: "forbidden",
|
|
275
|
-
...deniedDetail(this.kernel?.environment, `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend`)
|
|
276
|
+
...deniedDetail(this.kernel?.environment, `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend` + (hint === null ? "" : `. ${hint}`))
|
|
276
277
|
};
|
|
277
278
|
auditedPeer.notify("realtime:denied", denied);
|
|
278
279
|
}
|
package/docs/actions.md
CHANGED
|
@@ -359,7 +359,7 @@ donc réservée, quoi qu'en dise son décorateur. À savoir avant de nommer une
|
|
|
359
359
|
|
|
360
360
|
Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
|
|
361
361
|
configuration de sécurité — la même liste que pour les canaux
|
|
362
|
-
(`realtimeChannels`, `security/nodefony/config/config.ts:
|
|
362
|
+
(`realtimeChannels`, `security/nodefony/config/config.ts:1124`) :
|
|
363
363
|
|
|
364
364
|
```ts ignore
|
|
365
365
|
use("@nodefony/security", {
|
|
@@ -509,7 +509,7 @@ il sera conçu avec son premier consommateur réel.
|
|
|
509
509
|
Un cas particulier mérite d'être connu avant d'écrire une action : **elle existe peut-être déjà en
|
|
510
510
|
HTTP**. Le pont API expose la méthode `api.request`, qui rejoue une route de contrôleur sur la
|
|
511
511
|
socket, avec la même garde et le même résultat qu'en REST — `invokeApiRequest()`
|
|
512
|
-
(`RealtimeController.ts:
|
|
512
|
+
(`RealtimeController.ts:878`). Il est **désactivé par défaut** et s'active en surchargeant
|
|
513
513
|
`realtimeApiRequest()` (`RealtimeController.ts:219`).
|
|
514
514
|
|
|
515
515
|
```ts ignore
|
package/docs/architecture.md
CHANGED
|
@@ -126,7 +126,7 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
|
|
|
126
126
|
Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
|
|
127
127
|
classe `JsonRpcPeer` (`JsonRpcPeer.ts:271`) est du code isomorphe du cœur — le navigateur
|
|
128
128
|
l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
|
|
129
|
-
`RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
129
|
+
`RealtimeController.onHandshake()` (`RealtimeController.ts:328`). Le serveur peut donc
|
|
130
130
|
appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
|
|
131
131
|
déguisé.
|
|
132
132
|
|
|
@@ -141,7 +141,7 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
|
|
|
141
141
|
et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
|
|
142
142
|
toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
|
|
143
143
|
multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
|
|
144
|
-
`RealtimeController.requestClient()` (`RealtimeController.ts:
|
|
144
|
+
`RealtimeController.requestClient()` (`RealtimeController.ts:279`).
|
|
145
145
|
|
|
146
146
|
**Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
|
|
147
147
|
santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
|
|
@@ -154,7 +154,7 @@ chaîne de caractères. Passer de mono-process à multi-pods ne touche aucune li
|
|
|
154
154
|
|
|
155
155
|
**Le compromis, dit franchement** : un canal **ne franchit PAS** la frontière du process
|
|
156
156
|
tant que tu ne l'as pas déclaré. Le défaut est l'isolement (`RealtimeHub.publish()`,
|
|
157
|
-
`RealtimeHub.ts:
|
|
157
|
+
`RealtimeHub.ts:604`) — un choix de sûreté détaillé plus bas.
|
|
158
158
|
|
|
159
159
|
## 🚀 Démarrage rapide
|
|
160
160
|
|
|
@@ -388,7 +388,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
|
|
|
388
388
|
|
|
389
389
|
## 🔌 Le cycle de vie d'une connexion
|
|
390
390
|
|
|
391
|
-
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
391
|
+
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:328`), appelé
|
|
392
392
|
une seule fois par connexion, en chemin froid.
|
|
393
393
|
|
|
394
394
|
```mermaid
|
|
@@ -419,15 +419,15 @@ sequenceDiagram
|
|
|
419
419
|
|
|
420
420
|
Les étapes, dans l'ordre exact du code :
|
|
421
421
|
|
|
422
|
-
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:
|
|
422
|
+
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:970`). Refus →
|
|
423
423
|
fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
|
|
424
424
|
2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
|
|
425
|
-
(`RealtimeHub.ts:
|
|
425
|
+
(`RealtimeHub.ts:945`) parcourt les authentificateurs enregistrés : **le premier motif
|
|
426
426
|
qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
|
|
427
427
|
(`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
|
|
428
428
|
échec d'authentification ferme en `4001`.
|
|
429
429
|
3. **Création du peer et du transport**, puis association `peer → jeton`
|
|
430
|
-
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:
|
|
430
|
+
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1002`), stockée dans une `WeakMap` : le
|
|
431
431
|
jeton disparaît avec le peer, sans fuite.
|
|
432
432
|
4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
|
|
433
433
|
de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
|
|
@@ -459,13 +459,13 @@ déconnexion.
|
|
|
459
459
|
La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
|
|
460
460
|
figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
|
|
461
461
|
déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
|
|
462
|
-
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:
|
|
462
|
+
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:778`) — relit
|
|
463
463
|
périodiquement ces identités et coupe les sockets orphelines. Seules les identités
|
|
464
464
|
**révocables** y entrent : un visiteur anonyme ne coûte rien.
|
|
465
465
|
|
|
466
466
|
## Le hub — canaux partagés et fan-out
|
|
467
467
|
|
|
468
|
-
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:
|
|
468
|
+
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1435`). Il ne
|
|
469
469
|
dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
|
|
470
470
|
dépendances.
|
|
471
471
|
|
|
@@ -478,10 +478,10 @@ dépendances.
|
|
|
478
478
|
paquet du producteur atteigne bien ce premier abonné.
|
|
479
479
|
3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
|
|
480
480
|
registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
|
|
481
|
-
`RealtimeHub.ts:
|
|
481
|
+
`RealtimeHub.ts:1281`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
|
|
482
482
|
l'abonnement est refusé et rien n'est alloué.
|
|
483
483
|
|
|
484
|
-
Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:
|
|
484
|
+
Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
|
|
485
485
|
`dispose` du producteur et retire le canal. Un producteur fautif qui lève une exception ne
|
|
486
486
|
bloque pas le nettoyage.
|
|
487
487
|
|
|
@@ -496,12 +496,12 @@ Deux méthodes qui se ressemblent et ne font pas du tout la même chose :
|
|
|
496
496
|
|
|
497
497
|
| Méthode | Fan-out local | Propagation aux autres process | Qui l'appelle |
|
|
498
498
|
| --------------------------------------- | :-----------: | :--------------------------------: | ----------------------------------- |
|
|
499
|
-
| `publish()` (`RealtimeHub.ts:
|
|
500
|
-
| `publishLocal()` (`RealtimeHub.ts:
|
|
499
|
+
| `publish()` (`RealtimeHub.ts:604`) | oui | oui, **si** le canal est broadcast | producteurs, contrôleurs, services |
|
|
500
|
+
| `publishLocal()` (`RealtimeHub.ts:623`) | oui | **jamais** | l'arrivée d'un message du backplane |
|
|
501
501
|
|
|
502
502
|
C'est **la** règle qui empêche la tempête : un message reçu d'un pair est réinjecté
|
|
503
503
|
localement et **ne repart pas**. Le câblage se fait une fois pour toutes dans
|
|
504
|
-
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
504
|
+
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), qui branche l'arrivée du backplane
|
|
505
505
|
directement sur `publishLocal`.
|
|
506
506
|
|
|
507
507
|
Le fan-out lui-même (`RealtimeHub.ts:340`) est **isolé** : chaque livraison est protégée,
|
|
@@ -743,13 +743,13 @@ Deux propriétés architecturales méritent d'être notées ici, parce qu'elles
|
|
|
743
743
|
choix de conception visibles partout dans le module :
|
|
744
744
|
|
|
745
745
|
**Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
|
|
746
|
-
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:
|
|
746
|
+
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1042`, testé une
|
|
747
747
|
fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
|
|
748
748
|
chaud ne paie **rien** du tout.
|
|
749
749
|
|
|
750
750
|
**Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
|
|
751
751
|
politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
|
|
752
|
-
(`RealtimeHub.ts:
|
|
752
|
+
(`RealtimeHub.ts:1101`) le détecte et un avertissement est émis une fois par process
|
|
753
753
|
(`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
|
|
754
754
|
bien plus dangereux qu'un canal ouvertement public.
|
|
755
755
|
|
|
@@ -808,7 +808,7 @@ révocables, authentificateurs, politiques de canal, association peer→jeton :
|
|
|
808
808
|
`null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
|
|
809
809
|
|
|
810
810
|
**Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
|
|
811
|
-
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:
|
|
811
|
+
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:793`).
|
|
812
812
|
Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
|
|
813
813
|
process.
|
|
814
814
|
|
package/docs/configuration.md
CHANGED
|
@@ -457,13 +457,13 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
|
|
|
457
457
|
### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
|
|
458
458
|
|
|
459
459
|
Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
|
|
460
|
-
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:
|
|
460
|
+
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:970`). Une origine refusée ferme la socket avec le code
|
|
461
461
|
`4003`.
|
|
462
462
|
|
|
463
463
|
Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
|
|
464
464
|
active-le dès que ta socket est joignable depuis un navigateur.
|
|
465
465
|
|
|
466
|
-
Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts:
|
|
466
|
+
Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts:313`) :
|
|
467
467
|
|
|
468
468
|
| Situation | Résultat |
|
|
469
469
|
| ------------------------------------------------ | ------------------------------------------------------------------------ |
|
package/docs/cookbook-chat.md
CHANGED
|
@@ -1598,19 +1598,19 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1598
1598
|
| Ce que tu as utilisé | Où c'est écrit |
|
|
1599
1599
|
| --- | --- |
|
|
1600
1600
|
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:142`), `RealtimeInbound()` (`realtimeDecorators.ts:182`) |
|
|
1601
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:
|
|
1601
|
+
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:249`) |
|
|
1602
1602
|
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:219`) |
|
|
1603
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`), `RealtimeController.startChannel()` (`RealtimeController.ts:
|
|
1604
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
1603
|
+
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`), `RealtimeController.startChannel()` (`RealtimeController.ts:746`) |
|
|
1604
|
+
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:549`) |
|
|
1605
1605
|
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:594`) |
|
|
1606
|
-
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
1606
|
+
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
|
|
1607
1607
|
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1037`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1037`) |
|
|
1608
1608
|
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:253`) |
|
|
1609
1609
|
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:228`), erreur `unauthorized` (`JsonRpcPeer.ts:400`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:349`) |
|
|
1610
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
1610
|
+
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
|
|
1611
1611
|
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:56`), fermeture `slow consumer` (`RealtimeHub.ts:63`) |
|
|
1612
|
-
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:
|
|
1613
|
-
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:
|
|
1612
|
+
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:984`) |
|
|
1613
|
+
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
|
|
1614
1614
|
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:775`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
|
|
1615
1615
|
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
|
|
1616
1616
|
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:200`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
|
package/docs/observabilite.md
CHANGED
|
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
|
|
|
265
265
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
266
266
|
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
267
|
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
-
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:838`). |
|
|
269
269
|
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
270
|
|
|
271
271
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
package/docs/protocole.md
CHANGED
|
@@ -347,7 +347,7 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
|
|
|
347
347
|
|
|
348
348
|
> [!TIP]
|
|
349
349
|
> **L'accueil est ta carte du territoire.** `methods` et `channels` sont construits à partir de ce
|
|
350
|
-
> que l'endpoint expose réellement (`RealtimeController.ts:
|
|
350
|
+
> que l'endpoint expose réellement (`RealtimeController.ts:641`) : un client peut activer ou griser
|
|
351
351
|
> ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
|
|
352
352
|
> `socket.serverChannels`.
|
|
353
353
|
|
package/docs/securite.md
CHANGED
|
@@ -118,8 +118,8 @@ politiques.**
|
|
|
118
118
|
`security/nodefony/config/config.ts:109`). L'opt-out est explicite ; un opt-in aurait été
|
|
119
119
|
_fail-open_ (une zone qui oublie le flag laisserait le WS anonyme).
|
|
120
120
|
- Le verrou de frame consulte la **même** fonction de match que le HTTP, `Firewall.matchPath()`
|
|
121
|
-
(`firewall.ts:
|
|
122
|
-
(`firewall.ts:
|
|
121
|
+
(`firewall.ts:712`), et la **même** hiérarchie de rôles, `Firewall.hasRole()`
|
|
122
|
+
(`firewall.ts:482`). Invariant par construction : `api.request {path}` n'accorde jamais plus que
|
|
123
123
|
`GET {path}`.
|
|
124
124
|
- L'identité du handshake est celle du firewall HTTP : `FirewallRealtimeAuthenticator`
|
|
125
125
|
(`FirewallRealtimeAuthenticator.ts:57`) ne relit ni cookie ni base, il **promeut** l'`IUser` déjà
|
|
@@ -263,12 +263,12 @@ Nodefony pose **deux barrières successives**, à deux étages différents.
|
|
|
263
263
|
**Barrière 1 — transport, active sans rien faire.** `HttpKernel.checkWebsocketOrigin()`
|
|
264
264
|
(`http-kernel.ts:599`) exige que l'`Origin` du handshake corresponde au `Host` servi, avec tolérance
|
|
265
265
|
loopback en développement et une allowlist optionnelle (`allowedOrigins`,
|
|
266
|
-
`http/nodefony/config/config.ts:
|
|
266
|
+
`http/nodefony/config/config.ts:552`) acceptant le hostname exact ou un wildcard à un label. Une
|
|
267
267
|
requête **sans** `Origin` est acceptée : un attaquant non-navigateur n'a pas besoin de CSWSH.
|
|
268
268
|
|
|
269
269
|
**Barrière 2 — module realtime, opt-in et plus stricte.** `RealtimeHub.checkOrigin()`
|
|
270
|
-
(`RealtimeHub.ts:
|
|
271
|
-
(`RealtimeService.ts:
|
|
270
|
+
(`RealtimeHub.ts:970`) consulte une garde compilée une fois au boot par `buildOriginGuard()`
|
|
271
|
+
(`RealtimeService.ts:313`) depuis `checkOriginSchema` (`realtime/nodefony/config/config.ts:162`).
|
|
272
272
|
Trois différences comptent :
|
|
273
273
|
|
|
274
274
|
1. **Match exact scheme + host + port**, aucun wildcard. Durcissement volontaire par rapport au
|
|
@@ -292,13 +292,13 @@ n'est jamais traitée.
|
|
|
292
292
|
|
|
293
293
|
### Le pipeline exact
|
|
294
294
|
|
|
295
|
-
`RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
295
|
+
`RealtimeController.onHandshake()` (`RealtimeController.ts:328`) exécute, une fois par connexion :
|
|
296
296
|
|
|
297
297
|
1. Construction d'un DTO neutre `IRealtimeHandshake` par `buildHandshakeFromContext()`
|
|
298
|
-
(`RealtimeController.ts:
|
|
298
|
+
(`RealtimeController.ts:1113`) — headers, cookies aplatis, url, origin, sous-protocoles. Aucune
|
|
299
299
|
dépendance à `@nodefony/security` dans le contrat.
|
|
300
300
|
2. Contrôle d'origine (verrou 1).
|
|
301
|
-
3. Résolution de l'authenticator par `RealtimeHub.resolveAuthenticator()` (`RealtimeHub.ts:
|
|
301
|
+
3. Résolution de l'authenticator par `RealtimeHub.resolveAuthenticator()` (`RealtimeHub.ts:945`) :
|
|
302
302
|
les matchers sont testés dans l'ordre d'enregistrement, **le premier qui matche capture**.
|
|
303
303
|
4. `authenticator.authenticate(handshake)` — **async autorisé** (on est en cold path, une fois par
|
|
304
304
|
connexion : lire un store est acceptable ici, jamais par frame).
|
|
@@ -312,8 +312,8 @@ d'audit défectueux ne peut pas empêcher la fermeture.
|
|
|
312
312
|
|
|
313
313
|
### Les matchers — quelle porte, quel vigile
|
|
314
314
|
|
|
315
|
-
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:
|
|
316
|
-
`compileMatcher()` (`RealtimeHub.ts:
|
|
315
|
+
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:927`) associe un sélecteur à une stratégie.
|
|
316
|
+
`compileMatcher()` (`RealtimeHub.ts:1402`) le compile une fois :
|
|
317
317
|
|
|
318
318
|
- `pattern` chaîne → RegExp **préfixe ancrée**, méta-caractères échappés par `RegExp.escape`.
|
|
319
319
|
`"/admin/"` matche `/admin/` et `/admin/foo`, littéralement.
|
|
@@ -330,7 +330,7 @@ identité d'instance et une instance partagée n'enregistrerait que le premier m
|
|
|
330
330
|
|
|
331
331
|
### Zero Trust — il y a toujours un token
|
|
332
332
|
|
|
333
|
-
`RealtimeHub.getTokenForPeer()` (`RealtimeHub.ts:
|
|
333
|
+
`RealtimeHub.getTokenForPeer()` (`RealtimeHub.ts:1014`) ne renvoie **jamais** `null` : à défaut de
|
|
334
334
|
token posé, c'est `ANONYMOUS_REALTIME_TOKEN` (`AnonymousRealtimeToken.ts:18`), singleton gelé,
|
|
335
335
|
`isAuthenticated() === false`, `roles: ["ROLE_ANONYMOUS"]`. Le code consommateur (verrou, audit)
|
|
336
336
|
n'a jamais à écrire « et s'il n'y a pas de token ? ».
|
|
@@ -468,11 +468,11 @@ Trois durcissements méritent d'être connus :
|
|
|
468
468
|
`authenticated: true`. Le test porte sur le **namespace du canal**, pas sur le préfixe de la règle
|
|
469
469
|
qui a matché : un préfixe de config plus court ou altéré ne contourne rien.
|
|
470
470
|
- **La config passe avant les défauts** — les règles de `realtimeChannels`
|
|
471
|
-
(`security/nodefony/config/config.ts:
|
|
471
|
+
(`security/nodefony/config/config.ts:1124`) sont placées en tête, premier match gagnant. On peut
|
|
472
472
|
donc re-cibler `nodefony:syslog` sur `ROLE_SECURITY_AUDITOR` ; on ne peut pas l'ouvrir à l'anonyme.
|
|
473
473
|
|
|
474
474
|
Le canal du journal d'audit (`nodefony:audit`) est enregistré comme **canal système** sur le hub
|
|
475
|
-
(`RealtimeHub.registerSystemChannel()`, `RealtimeHub.ts:
|
|
475
|
+
(`RealtimeHub.registerSystemChannel()`, `RealtimeHub.ts:1281`) : il devient servable par n'importe
|
|
476
476
|
quel endpoint realtime, sans qu'aucun controller ne le connaisse — et il est gardé par sa règle
|
|
477
477
|
dédiée. Son enregistrement est **couplé** à la pose du verrou (même condition), donc il n'existe
|
|
478
478
|
jamais de canal d'audit non gardé.
|
|
@@ -486,12 +486,12 @@ cumulatifs (ET) ; un axe absent n'impose rien :
|
|
|
486
486
|
| Axe | Sens | Évalué par |
|
|
487
487
|
| --------------- | ----------------------------------------------------------------------------- | ---------------------------------------- |
|
|
488
488
|
| `authenticated` | token non anonyme | `satisfies()` (`frameAuthorizer.ts:240`) |
|
|
489
|
-
| `roles` | un des rôles suffit, **hiérarchie comprise** | `Firewall.hasRole()` (`firewall.ts:
|
|
489
|
+
| `roles` | un des rôles suffit, **hiérarchie comprise** | `Firewall.hasRole()` (`firewall.ts:482`) |
|
|
490
490
|
| `scopes` | un des scopes suffit — axe API (JWT, clé API), une session BFF n'en porte pas | comparaison directe |
|
|
491
491
|
|
|
492
492
|
Une policy **vide** n'est pas enregistrée : `definePolicy()` (`realtimeDecorators.ts:40`) ignore un
|
|
493
493
|
objet sans contrainte — le canal reste libre, le registre reste vide. Les déclarations sont publiées
|
|
494
|
-
au hub **au handshake**, pas au boot (`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts:
|
|
494
|
+
au hub **au handshake**, pas au boot (`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts:1127`,
|
|
495
495
|
idempotent) ; le décideur les relit par `RealtimeService.resolveChannelPolicy()`
|
|
496
496
|
(`RealtimeService.ts:277`).
|
|
497
497
|
|
|
@@ -508,7 +508,7 @@ là, le canal fonctionne, et le refus attendu n'arrive jamais.
|
|
|
508
508
|
|
|
509
509
|
Le serveur le **dit** désormais. Au premier canal servi dynamiquement sans politique, alors que le
|
|
510
510
|
module en déclare pour d'autres canaux, un avertissement est journalisé une fois
|
|
511
|
-
(`RealtimeHub.noticeUnguardedDynamicChannel()`, `RealtimeHub.ts:
|
|
511
|
+
(`RealtimeHub.noticeUnguardedDynamicChannel()`, `RealtimeHub.ts:1254`) :
|
|
512
512
|
|
|
513
513
|
```
|
|
514
514
|
le canal "chat:room:1000" est servi par une fabrique DYNAMIQUE et n'est couvert par AUCUNE
|
|
@@ -537,14 +537,14 @@ C'est exactement le test de `Firewall.#wireRealtime()` (`firewall.ts:253`) : san
|
|
|
537
537
|
|
|
538
538
|
Deuxième subtilité : `beforeDispatch` n'est branché sur une connexion que si le verrou est **déjà**
|
|
539
539
|
posé au moment de son handshake (`RealtimeController.ts:429`, via
|
|
540
|
-
`RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:
|
|
540
|
+
`RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:1042`). Choix de performance délibéré (un hub
|
|
541
541
|
non sécurisé garde un coût nul par frame), mais avec une conséquence : **une connexion ouverte avant
|
|
542
542
|
la pose du verrou n'est jamais gardée**, et ce jusqu'à sa fermeture. En fonctionnement normal le
|
|
543
543
|
firewall se construit au boot, avant tout trafic ; le cas ne se présente qu'en appelant
|
|
544
544
|
`setFrameAuthorizer()` à chaud.
|
|
545
545
|
|
|
546
546
|
**Le refus de dégrader en silence.** Quand des policies sont déclarées sans décideur câblé,
|
|
547
|
-
`RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:
|
|
547
|
+
`RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1101`) renvoie `true` et le controller
|
|
548
548
|
émet un WARNING explicite, une seule fois par process (`RealtimeController.ts:522`) :
|
|
549
549
|
|
|
550
550
|
```text
|
|
@@ -600,8 +600,8 @@ Nodefony ferme l'écart par deux mécanismes de granularité différente.
|
|
|
600
600
|
|
|
601
601
|
| Surface | Re-validation | Fenêtre d'exposition | Où |
|
|
602
602
|
| --------------------------- | -------------------- | -------------------- | --------------------------------------------------------------------- |
|
|
603
|
-
| `api.request` (data plane) | **à chaque frame** | nulle | `RealtimeController.invokeApiRequest()` (`RealtimeController.ts:
|
|
604
|
-
| `subscribe` / flux de canal | **périodique**, 30 s | ≤ 30 s | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
603
|
+
| `api.request` (data plane) | **à chaque frame** | nulle | `RealtimeController.invokeApiRequest()` (`RealtimeController.ts:878`) |
|
|
604
|
+
| `subscribe` / flux de canal | **périodique**, 30 s | ≤ 30 s | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
|
|
605
605
|
|
|
606
606
|
**Sur `api.request`**, `token.isValid()` est appelé avant l'exécution de l'action ; identité périmée
|
|
607
607
|
ou changée → `-32000` avec `status: 401`, et le client bascule sur un `fetch` HTTP porteur du cookie
|
|
@@ -609,7 +609,7 @@ courant. Une erreur de re-validation vaut refus (fail-closed).
|
|
|
609
609
|
|
|
610
610
|
**Sur les canaux**, le hub n'inscrit au registre de révocation que les connexions dont le token porte
|
|
611
611
|
`isValid` (`RealtimeController.ts:550`) — anonymes et JWT n'y entrent jamais, coût nul.
|
|
612
|
-
`RealtimeHub.registerRevocable()` (`RealtimeHub.ts:
|
|
612
|
+
`RealtimeHub.registerRevocable()` (`RealtimeHub.ts:778`) démarre un `setInterval` `unref` au premier
|
|
613
613
|
inscrit et l'arrête dès que le registre se vide : zéro timer au repos. Période :
|
|
614
614
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`), 30 s, alignée sur le heartbeat WS.
|
|
615
615
|
|
|
@@ -674,10 +674,10 @@ Un onglet en arrière-plan, un mobile en zone blanche, une fenêtre TCP pleine :
|
|
|
674
674
|
grossit sans borne, et le multiplexage concentre le risque (une socket lente bloque tous ses
|
|
675
675
|
canaux). `WsConnectionTransport.send()` (`WsConnectionTransport.ts:76`) applique deux seuils :
|
|
676
676
|
|
|
677
|
-
| `bufferedAmount`
|
|
678
|
-
|
|
|
679
|
-
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:
|
|
680
|
-
| `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut
|
|
677
|
+
| `bufferedAmount` | Action |
|
|
678
|
+
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
679
|
+
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1044`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
|
|
680
|
+
| `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
|
|
681
681
|
|
|
682
682
|
### Taille des messages entrants
|
|
683
683
|
|
|
@@ -724,7 +724,7 @@ Source unique des défauts : `realtimeConfigSchema` (`realtime/nodefony/config/c
|
|
|
724
724
|
Cette section existe pour éviter une confiance mal placée. Chaque point est vérifié au code.
|
|
725
725
|
|
|
726
726
|
**1. Il n'y a pas de frontière de canal entre modules.** Un nom de canal est une chaîne dans un
|
|
727
|
-
registre global (`#channelPolicies` du hub singleton, `RealtimeHub.ts:
|
|
727
|
+
registre global (`#channelPolicies` du hub singleton, `RealtimeHub.ts:325`). Rien n'empêche le
|
|
728
728
|
controller du module A de servir un canal au nom d'un canal du module B, ni de déclarer une policy
|
|
729
729
|
plus faible sur ce même nom — la déclaration la plus récente écrase (`RealtimeHub.ts:751`, idempotent
|
|
730
730
|
par écrasement). En pratique, l'exploitation exige que le controller fautif accepte le nom (sa
|
package/docs/vocabulaire.md
CHANGED
|
@@ -255,7 +255,7 @@ que personne ne lit ; écouter sans s'abonner n'en reçoit aucune.
|
|
|
255
255
|
client, la charge part vers le serveur (un seul pair) ; depuis le serveur, elle part en
|
|
256
256
|
[fan-out](#fan-out--une-publication-n-livraisons) vers tous les abonnés.
|
|
257
257
|
|
|
258
|
-
`IRealtimeSocket.publish()` (`IRealtimeSocket.ts:160`), `RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
258
|
+
`IRealtimeSocket.publish()` (`IRealtimeSocket.ts:160`), `RealtimeHub.publish()` (`RealtimeHub.ts:604`).
|
|
259
259
|
|
|
260
260
|
### `action RPC` (request) — l'appel corrélé
|
|
261
261
|
|
|
@@ -297,7 +297,7 @@ importable **sans** aucune dépendance serveur, ce qui est la condition de l'iso
|
|
|
297
297
|
(`ServerRealtimeSocket.ts:223`) : un service métier tient un handle et publie **comme une page
|
|
298
298
|
front**. Une différence assumée : `request()` n'y est pas supporté — au-dessus du hub il n'y a pas
|
|
299
299
|
**un** pair mais N clients. Pour un appel serveur → un client précis, c'est `requestClient()`
|
|
300
|
-
(`RealtimeController.ts:
|
|
300
|
+
(`RealtimeController.ts:279`).
|
|
301
301
|
|
|
302
302
|
### `accueil` (welcome) — la première frame
|
|
303
303
|
|
|
@@ -305,7 +305,7 @@ La notification `realtime:welcome`, poussée par le serveur juste après le hand
|
|
|
305
305
|
protocole, les canaux et actions **découvrables** de l'endpoint, et l'identité résolue. Un client
|
|
306
306
|
attend cette frame avant de pousser quoi que ce soit.
|
|
307
307
|
|
|
308
|
-
`IRealtimeWelcome` (`RealtimeEventMap.ts:
|
|
308
|
+
`IRealtimeWelcome` (`RealtimeEventMap.ts:231`).
|
|
309
309
|
|
|
310
310
|
### `identité` — qui est cette connexion
|
|
311
311
|
|
|
@@ -313,7 +313,7 @@ La vue « sur soi » d'une connexion : type de jeton, authentifié ou non, ident
|
|
|
313
313
|
Aucun secret — seulement ce que le porteur sait déjà de lui-même. Elle est **résolue une fois** au
|
|
314
314
|
handshake, jamais renégociée par frame.
|
|
315
315
|
|
|
316
|
-
`RealtimeIdentity` (`RealtimeEventMap.ts:
|
|
316
|
+
`RealtimeIdentity` (`RealtimeEventMap.ts:212`).
|
|
317
317
|
|
|
318
318
|
## 🏗️ Le serveur — ce qui aiguille derrière la prise
|
|
319
319
|
|
|
@@ -335,7 +335,7 @@ handshake, jamais renégociée par frame.
|
|
|
335
335
|
### `hub` — le broker du process
|
|
336
336
|
|
|
337
337
|
Le **standard téléphonique** du pod : il tient la table « canal → abonnés locaux » et diffuse. Un
|
|
338
|
-
process = un hub, obtenu par `getRealtimeHub()` (`RealtimeHub.ts:
|
|
338
|
+
process = un hub, obtenu par `getRealtimeHub()` (`RealtimeHub.ts:1435`). Il ne connaît **ni** les
|
|
339
339
|
contrôleurs, **ni** le métier : ce sont les providers qui portent les dépendances.
|
|
340
340
|
|
|
341
341
|
`RealtimeHub` (`RealtimeHub.ts:213`). ⚠️ « hub » désigne **toujours** le serveur ; ce que tient le
|
|
@@ -346,7 +346,7 @@ code applicatif s'appelle une [socket](#socket--la-prise-que-tient-ton-code).
|
|
|
346
346
|
L'action de diffuser : une charge publiée sur un canal part vers **tous** les sinks abonnés
|
|
347
347
|
localement. Une connexion fautive ne casse pas la diffusion aux autres — chaque livraison est isolée.
|
|
348
348
|
|
|
349
|
-
`RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
349
|
+
`RealtimeHub.publish()` (`RealtimeHub.ts:604`) pour le chemin complet, `publishLocal()`
|
|
350
350
|
(`RealtimeHub.ts:549`) pour la diffusion **strictement locale** (voie d'entrée du backplane).
|
|
351
351
|
|
|
352
352
|
### `sink` — le point de livraison d'une connexion
|
|
@@ -354,7 +354,7 @@ localement. Une connexion fautive ne casse pas la diffusion aux autres — chaqu
|
|
|
354
354
|
Un `sink` = **une** connexion sur **un** canal : la fonction qui pousse la charge vers son pair. Le
|
|
355
355
|
mot vient du couple source/puits : le provider est la source, le sink le puits.
|
|
356
356
|
|
|
357
|
-
`ChannelSink` (`RealtimeHub.ts:
|
|
357
|
+
`ChannelSink` (`RealtimeHub.ts:155`). Le nombre de sinks d'un canal est son nombre d'abonnés locaux.
|
|
358
358
|
|
|
359
359
|
### `provider` — ce qui produit un canal
|
|
360
360
|
|
|
@@ -362,7 +362,7 @@ La fabrique appelée **au premier abonné** d'un canal, qui démarre ce qui prod
|
|
|
362
362
|
ticker, un écouteur) et rend son `dispose`, appelé **au dernier désabonné**. Zéro abonné = zéro
|
|
363
363
|
timer.
|
|
364
364
|
|
|
365
|
-
`ChannelFactory` (`RealtimeHub.ts:
|
|
365
|
+
`ChannelFactory` (`RealtimeHub.ts:170`). ⚠️ Un provider est **partagé** : il survit à la connexion qui
|
|
366
366
|
l'a créé — n'y capturer que des dépendances à longue vie, jamais le contexte d'une connexion.
|
|
367
367
|
|
|
368
368
|
### `canal partagé` — un provider par canal et par pod
|
|
@@ -386,7 +386,7 @@ Sans cette distinction, le premier service qui prononce un nom de canal l'ouvrir
|
|
|
386
386
|
monde — et les clients suivants s'abonneraient à un flux que rien ne produit, sans jamais l'apprendre.
|
|
387
387
|
|
|
388
388
|
`RealtimeHub.listen()` (`RealtimeHub.ts:455`) pour l'écoute, `isChannelOwned()`
|
|
389
|
-
(`RealtimeHub.ts:
|
|
389
|
+
(`RealtimeHub.ts:550`) pour lire l'état. ⚠️ Un canal passif **n'est pas** un canal fermé : il n'a
|
|
390
390
|
simplement pas encore de propriétaire.
|
|
391
391
|
|
|
392
392
|
### `canal système` — servi par la plateforme
|
|
@@ -395,7 +395,7 @@ Un canal dont la fabrique est déclarée par un module bas niveau (le journal d'
|
|
|
395
395
|
qui devient servable par **n'importe quel** endpoint, présent ou futur, sans qu'aucun contrôleur ne
|
|
396
396
|
le connaisse. Consulté seulement quand la fabrique du contrôleur a dit « inconnu ».
|
|
397
397
|
|
|
398
|
-
`RealtimeHub.registerSystemChannel()` (`RealtimeHub.ts:
|
|
398
|
+
`RealtimeHub.registerSystemChannel()` (`RealtimeHub.ts:1281`).
|
|
399
399
|
|
|
400
400
|
### `canal broadcast` — celui qui traverse le backplane
|
|
401
401
|
|
|
@@ -412,7 +412,7 @@ Tout canal **non déclaré** broadcast reste dans son process. C'est voulu : les
|
|
|
412
412
|
d'observabilité (journaux, sondes, état interne) décrivent **ce pod**, et les agréger silencieusement
|
|
413
413
|
serait à la fois faux et une fuite. Traverser le process est une capacité qu'un canal **demande**.
|
|
414
414
|
|
|
415
|
-
Politique de forward du hub — `#broadcastPrefixes` (`RealtimeHub.ts:
|
|
415
|
+
Politique de forward du hub — `#broadcastPrefixes` (`RealtimeHub.ts:393`).
|
|
416
416
|
|
|
417
417
|
### `contrôleur temps réel` — l'endpoint WebSocket
|
|
418
418
|
|
|
@@ -420,7 +420,7 @@ La classe de base d'un endpoint : elle porte tout le protocole (handshake, accue
|
|
|
420
420
|
des frames, cycle de vie des canaux) et ne laisse au métier que ses canaux et ses actions.
|
|
421
421
|
|
|
422
422
|
`RealtimeController` (`RealtimeController.ts:144`), point d'entrée `handleRealtime()`
|
|
423
|
-
(`RealtimeController.ts:
|
|
423
|
+
(`RealtimeController.ts:249`). C'est un **contrôleur** au sens habituel de Nodefony : la même classe
|
|
424
424
|
peut porter des routes HTTP.
|
|
425
425
|
|
|
426
426
|
### `service realtime` — la façade d'injection
|
|
@@ -438,7 +438,7 @@ contrôleur** que celle servie en REST, avec **la même garde**. Le pont n'attei
|
|
|
438
438
|
déclarent explicitement le transport WebSocket — aucun contournement possible.
|
|
439
439
|
|
|
440
440
|
`realtimeApiRequest()` (`RealtimeController.ts:219`), mise en œuvre `invokeApiRequest()`
|
|
441
|
-
(`RealtimeController.ts:
|
|
441
|
+
(`RealtimeController.ts:878`). Désactivé par défaut.
|
|
442
442
|
|
|
443
443
|
## 🔌 Le protocole et le transport — ce qui passe sur le fil
|
|
444
444
|
|
|
@@ -535,7 +535,7 @@ publications d'un process aux autres. Un contrat volontairement minuscule — `p
|
|
|
535
535
|
`start`, `stop`, `describe` — parce que tout ce qui est riche appartient au hub.
|
|
536
536
|
|
|
537
537
|
`IBackplane` (`IBackplane.ts:75`), message `IBackplaneMessage` (`IBackplane.ts:51`), branchement
|
|
538
|
-
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
538
|
+
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`).
|
|
539
539
|
|
|
540
540
|
> [!TIP]
|
|
541
541
|
> C'est la pièce qui tient la promesse « une ligne de configuration change tout » : le hub **ne sait
|
|
@@ -587,7 +587,7 @@ La règle qui empêche les tempêtes : un message **reçu** du backplane est ré
|
|
|
587
587
|
**locale seulement**, jamais renvoyé sur le bus ; et un message qui porte sa propre origine est
|
|
588
588
|
ignoré à l'arrivée. Sans l'origine, l'anti-écho ferait taire le voisin au lieu de soi-même.
|
|
589
589
|
|
|
590
|
-
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:
|
|
590
|
+
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`).
|
|
591
591
|
|
|
592
592
|
### `cloison` (namespace) — deux applications, un même bus
|
|
593
593
|
|
|
@@ -630,7 +630,7 @@ l'authenticator tourne et que l'identité est figée. Tout ce qui coûte cher se
|
|
|
630
630
|
frame.
|
|
631
631
|
|
|
632
632
|
DTO neutre `IRealtimeHandshake` (`IRealtimeHandshake.ts:14`) — en-têtes, cookies, URL, origine,
|
|
633
|
-
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:
|
|
633
|
+
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:328`).
|
|
634
634
|
|
|
635
635
|
### `authenticator` — du handshake à l'identité
|
|
636
636
|
|
|
@@ -639,7 +639,7 @@ La stratégie qui transforme une poignée de main en identité : `supports` (est
|
|
|
639
639
|
HTTP — un seul modèle mental pour les deux transports.
|
|
640
640
|
|
|
641
641
|
`IRealtimeAuthenticator` (`IRealtimeAuthenticator.ts:24`), enregistrement
|
|
642
|
-
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:
|
|
642
|
+
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:927`).
|
|
643
643
|
|
|
644
644
|
### `matcher` — qui capture quoi
|
|
645
645
|
|
|
@@ -647,7 +647,7 @@ Le sélecteur (motif d'URL, hôte optionnel) qui décide **quel** authenticator
|
|
|
647
647
|
**premier** qui capture gagne : on enregistre donc du plus spécifique au plus général.
|
|
648
648
|
|
|
649
649
|
`IRealtimeAuthenticatorMatcher` (`IRealtimeAuthenticatorMatcher.ts:25`), résolution
|
|
650
|
-
`resolveAuthenticator()` (`RealtimeHub.ts:
|
|
650
|
+
`resolveAuthenticator()` (`RealtimeHub.ts:945`).
|
|
651
651
|
|
|
652
652
|
### `jeton` (token) — l'identité figée
|
|
653
653
|
|
|
@@ -664,7 +664,7 @@ Les exigences déclarées d'un canal : authentifié, rôles, scopes. Elles s'att
|
|
|
664
664
|
canal, pas à la méthode qui l'implémente — parce que c'est le nom que le pare-feu résout.
|
|
665
665
|
|
|
666
666
|
`IChannelPolicy` (`IChannelPolicy.ts:20`), déclaration par `@RealtimeChannel`
|
|
667
|
-
(`realtimeDecorators.ts:192`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:
|
|
667
|
+
(`realtimeDecorators.ts:192`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:1212`).
|
|
668
668
|
|
|
669
669
|
### `verrou de frame` — la décision par frame
|
|
670
670
|
|
|
@@ -672,8 +672,8 @@ La fonction qui répond « cette frame passe-t-elle ? » à partir du jeton **d
|
|
|
672
672
|
**Synchrone par doctrine** : attendre une réponse distante à chaque frame sérialiserait le flux d'une
|
|
673
673
|
connexion. Un contrôle qui doit interroger le réseau se fait au handshake, pas ici.
|
|
674
674
|
|
|
675
|
-
`FrameAuthorizer` (`RealtimeHub.ts:39`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:
|
|
676
|
-
`runAuthorizer()` (`RealtimeHub.ts:
|
|
675
|
+
`FrameAuthorizer` (`RealtimeHub.ts:39`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:1023`), appel
|
|
676
|
+
`runAuthorizer()` (`RealtimeHub.ts:1312`).
|
|
677
677
|
|
|
678
678
|
### `contrôle d'origine` — la défense d'ouverture
|
|
679
679
|
|
|
@@ -689,7 +689,7 @@ Le problème que ce mot nomme : une connexion ouverte pourrait survivre à la se
|
|
|
689
689
|
autorisée, puisque le verrou de frame ne relit rien. Un contrôle périodique referme l'écart en
|
|
690
690
|
re-validant les identités révocables et en fermant celles qui ne valent plus.
|
|
691
691
|
|
|
692
|
-
`registerRevocable()` (`RealtimeHub.ts:
|
|
692
|
+
`registerRevocable()` (`RealtimeHub.ts:778`), `revalidateRevocable()` (`RealtimeHub.ts:810`), période
|
|
693
693
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`). Une re-validation en erreur **ferme** — jamais
|
|
694
694
|
l'inverse.
|
|
695
695
|
|
|
@@ -699,7 +699,7 @@ Une notification refusée serait abandonnée en silence, laissant le client aveu
|
|
|
699
699
|
donc `realtime:denied`. Le motif est **générique** : jamais le rôle ou le scope manquant, qui
|
|
700
700
|
transformerait le refus en oracle d'autorisation.
|
|
701
701
|
|
|
702
|
-
`IRealtimeDenied` (`RealtimeEventMap.ts:
|
|
702
|
+
`IRealtimeDenied` (`RealtimeEventMap.ts:269`), conversion en message d'interface `deniedToNotice()`
|
|
703
703
|
(`notice.ts:45`).
|
|
704
704
|
|
|
705
705
|
### `audit de frame` — tracer le notable
|
|
@@ -728,7 +728,7 @@ ressources ; sans borne, une seule connexion peut les épuiser. Au-delà, l'abon
|
|
|
728
728
|
client en est informé.
|
|
729
729
|
|
|
730
730
|
`limits.maxChannelsPerConnection` (`config.ts:142`), pose `setMaxChannelsPerConnection()`
|
|
731
|
-
(`RealtimeHub.ts:
|
|
731
|
+
(`RealtimeHub.ts:979`).
|
|
732
732
|
|
|
733
733
|
## 📡 L'observabilité — ce que la socket dit d'elle-même
|
|
734
734
|
|
|
@@ -762,7 +762,7 @@ n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent
|
|
|
762
762
|
Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`) — une constante, car il ne
|
|
763
763
|
décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
|
|
764
764
|
`websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
|
|
765
|
-
`http/nodefony/config/config.ts:
|
|
765
|
+
`http/nodefony/config/config.ts:1044`).
|
|
766
766
|
|
|
767
767
|
### `consommateur lent` — la connexion qui décroche
|
|
768
768
|
|
|
@@ -807,9 +807,9 @@ elle-même par le même chemin que les autres modules.
|
|
|
807
807
|
| Deux déploiements se mélangent sur un même serveur Redis | pas de **cloison** — le numéro de base ne cloisonne pas le pub/sub | Poser `backplane.namespace` (`config.ts:59`) |
|
|
808
808
|
| Le fan-out disparaît entre conteneurs identiques | **origine** dérivée du seul identifiant de processus : deux conteneurs sont tous deux le n° 1, l'anti-écho avale tout | `resolveBackplaneOriginId()` (`originId.ts:24`) dérive du pod ou de l'hôte |
|
|
809
809
|
| Deux tickers pour le même tableau de bord | **cadence** différente = **canal** différent, jamais réconcilié | Fabriquer le nom via `rateChannel()` (`channelRate.ts:44`) des deux côtés |
|
|
810
|
-
| Un canal se croit protégé mais laisse tout passer | **politique** déclarée sans **verrou de frame** posé pour la faire respecter | Le hub le détecte et avertit : `hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:
|
|
810
|
+
| Un canal se croit protégé mais laisse tout passer | **politique** déclarée sans **verrou de frame** posé pour la faire respecter | Le hub le détecte et avertit : `hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1101`) |
|
|
811
811
|
| Une connexion garde ses flux après une déconnexion applicative | le **verrou de frame** ne relit rien : par construction, il ne voit pas la session mourir | Inscrire l'identité au registre de **révocation** (`RealtimeHub.ts:433`) |
|
|
812
|
-
| « Le provider a planté après la fermeture d'un onglet » | **provider** confondu avec **sink** : le provider est partagé, il survit à la connexion créatrice | N'y capturer que des dépendances à longue vie (`ChannelFactory`, `RealtimeHub.ts:
|
|
812
|
+
| « Le provider a planté après la fermeture d'un onglet » | **provider** confondu avec **sink** : le provider est partagé, il survit à la connexion créatrice | N'y capturer que des dépendances à longue vie (`ChannelFactory`, `RealtimeHub.ts:170`) |
|
|
813
813
|
| « Notre store realtime est indisponible » | **store** employé pour **driver** : le backplane ne conserve rien, il fait passer | Dire « le driver de backplane » — les données ont des stores, les flux ont des drivers |
|
|
814
814
|
|
|
815
815
|
> [!CAUTION]
|