@nodefony/realtime 10.0.0-beta.4 → 10.0.0-beta.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/README.md +79 -10
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +79 -12
- package/dist/nodefony/config/config.js +7 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +125 -19
- package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
- package/dist/nodefony/src/backplane/RedisBackplane.js +143 -31
- package/dist/nodefony/src/backplane/controlOrder.js +57 -0
- package/dist/nodefony/src/backplane/envelope.js +113 -12
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
- package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
- package/dist/nodefony/src/server/RealtimeConnection.js +110 -0
- package/dist/nodefony/src/server/RealtimeController.js +344 -85
- package/dist/nodefony/src/server/RealtimeHub.js +604 -61
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
- package/dist/nodefony/src/server/admissionBudget.js +176 -0
- package/dist/nodefony/src/server/channelPolicyRegistry.js +73 -0
- package/dist/nodefony/src/server/connectionAdmission.js +242 -0
- package/dist/nodefony/src/server/contractRoutes.js +85 -0
- package/dist/nodefony/src/server/deniedDetail.js +10 -3
- package/dist/nodefony/src/server/frameAdmission.js +168 -0
- package/dist/nodefony/src/server/subscribedAck.js +10 -1
- package/dist/nodefony/src/server/syslogUplink.js +3 -4
- package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
- package/dist/nodefony/src/service/RealtimeService.js +42 -1
- package/dist/nodefony/src/service/publicOrigin.js +40 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
- package/dist/nodefony/testing/backplaneConformance.js +271 -0
- package/dist/nodefony/testing/index.js +21 -4
- package/dist/types/index.d.ts +34 -7
- package/dist/types/nodefony/config/config.d.ts +9 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +86 -6
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +34 -50
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
- package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +79 -16
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +52 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +34 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
- package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +17 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +93 -13
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +321 -24
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
- package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +34 -0
- package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +70 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +7 -0
- package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +10 -2
- package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +66 -0
- package/dist/types/nodefony/testing/index.d.ts +17 -4
- package/docs/actions.md +442 -19
- package/docs/architecture.md +83 -8
- package/docs/cadence-et-contre-pression.md +87 -11
- package/docs/canaux-a-membres.md +267 -0
- package/docs/cas-d-usage.md +1702 -0
- package/docs/configuration.md +137 -17
- package/docs/cookbook-chat.md +333 -85
- package/docs/index.md +11 -1
- package/docs/observabilite.md +71 -14
- package/docs/protocole.md +133 -24
- package/docs/securite.md +294 -33
- package/docs/vocabulaire.md +13 -12
- package/package.json +14 -11
package/docs/securite.md
CHANGED
|
@@ -310,7 +310,13 @@ n'est jamais traitée.
|
|
|
310
310
|
3. Résolution de l'authenticator par `RealtimeHub.resolveAuthenticator()` (`RealtimeHub.ts#RealtimeHub.resolveAuthenticator`) :
|
|
311
311
|
les matchers sont testés dans l'ordre d'enregistrement, **le premier qui matche capture**.
|
|
312
312
|
4. `authenticator.authenticate(handshake)` — **async autorisé** (on est en cold path, une fois par
|
|
313
|
-
connexion : lire un store est acceptable ici, jamais par frame)
|
|
313
|
+
connexion : lire un store est acceptable ici, jamais par frame), et **borné** par
|
|
314
|
+
`authenticateTimeoutMs` (10 s par défaut, `admissionBudget.ts#AdmissionBudget.authenticateWithin`).
|
|
315
|
+
Au-delà, la socket est fermée en `1013` « try again later » : le client recule puis revient, et une
|
|
316
|
+
identité arrivée après coup est ignorée. Ni `onFailure` ni `4001` — une base en panne n'est pas un
|
|
317
|
+
refus d'identité, et un code définitif arrêterait un client légitime à chaque panne. Compteur
|
|
318
|
+
`authenticateTimedOutTotal`, un `WARNING` par fenêtre de 10 s. Un client parti pendant
|
|
319
|
+
l'authentification n'est jamais inscrit au hub.
|
|
314
320
|
5. Pose du token sur la WeakMap `peer → token` via `RealtimeHub.setTokenForPeer()`
|
|
315
321
|
(`RealtimeHub.ts#RealtimeHub.setTokenForPeer`), **avant** l'envoi du `welcome` : le lookup est garanti dès la première
|
|
316
322
|
frame.
|
|
@@ -499,9 +505,12 @@ cumulatifs (ET) ; un axe absent n'impose rien :
|
|
|
499
505
|
| `scopes` | un des scopes suffit — axe API (JWT, clé API), une session BFF n'en porte pas | comparaison directe |
|
|
500
506
|
|
|
501
507
|
Une policy **vide** n'est pas enregistrée : `definePolicy()` (`realtimeDecorators.ts#definePolicy`) ignore un
|
|
502
|
-
objet sans contrainte — le canal reste libre, le registre reste vide. Les déclarations sont
|
|
503
|
-
au hub **au
|
|
504
|
-
|
|
508
|
+
objet sans contrainte — le canal reste libre, le registre reste vide. Les déclarations sont inscrites
|
|
509
|
+
au hub **au démarrage** (`channelPolicyRegistry.ts#registerControllerPolicies`, à `onKernelReady`) :
|
|
510
|
+
un exemplaire qui n'a encore vu aucune poignée de main a déjà ses politiques — celui qui n'émet
|
|
511
|
+
que des webhooks aussi. Un contrôleur échappé au démarrage s'inscrit à sa première poignée de main
|
|
512
|
+
(`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts#RealtimeHub.registerChannelPolicy`, idempotent) ;
|
|
513
|
+
le décideur les relit par `RealtimeService.resolveChannelPolicy()`
|
|
505
514
|
(`RealtimeService.ts#RealtimeService.resolveChannelPolicy`).
|
|
506
515
|
|
|
507
516
|
### ⚠️ Une policy est attachée à un nom EXACT — les canaux dérivés n'héritent pas
|
|
@@ -539,6 +548,213 @@ Deux façons de fermer, selon l'intention :
|
|
|
539
548
|
> Le nom exact l'emporte sur un motif ; entre deux motifs, le plus spécifique gagne. Un `*` dans un
|
|
540
549
|
> nom souscrit est refusé : il désigne un motif de politique, jamais un canal.
|
|
541
550
|
|
|
551
|
+
### 📬 Le canal personnel `user:<id>` — fourni, rien à déclarer
|
|
552
|
+
|
|
553
|
+
Livrer un message à **une** personne — sur tous ses onglets, sur tous les serveurs, et à personne
|
|
554
|
+
d'autre — est une brique du framework. L'application n'écrit ni canal, ni règle, ni déclaration de
|
|
555
|
+
diffusion :
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// côté serveur — un service, un job, un contrôleur
|
|
559
|
+
getRealtimeHub<ChatContract>().publishToUser(bob.id, message);
|
|
560
|
+
|
|
561
|
+
// côté navigateur — cet onglet s'abonne quand son écran écoute
|
|
562
|
+
useEffect(() => chat.onUser((m) => addPrivate(m)), []);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
La forme du message se déclare dans le contrat, par la clé gabarit du canal personnel :
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
interface ChatContract extends IRealtimeContract {
|
|
569
|
+
serverToClient: {
|
|
570
|
+
[channel: `user:${string}`]: { from: string; text: string };
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Ce que le framework garantit, écrit une fois :
|
|
576
|
+
|
|
577
|
+
| Garantie | Comment |
|
|
578
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
579
|
+
| **Toi seul** | Politique intégrée : connexion authentifiée, admission par **égalité de chaîne** avec `user:<ton id>` (`userChannel.ts#isOwnUserChannel`). Jamais un découpage : l'identité `4` n'entre pas sur `user:42`. |
|
|
580
|
+
| **Aucun oracle** | Le verrou passe avant la fabrique, et la règle ne lit que l'identité du demandeur, jamais une base : `user:999` (inexistant) et `user:bob` (pas le tien) reçoivent le **même** `forbidden`. |
|
|
581
|
+
| **Tous les serveurs** | Dès qu'un bus est branché, le préfixe `user:` est diffusé par le framework (`RealtimeHub.ts#RealtimeHub.setBackplane`). |
|
|
582
|
+
| **Sans sécurité chargée** | La règle s'applique quand même : `allow` ne fait que retirer des abonnés. |
|
|
583
|
+
| **Jamais d'office** | Rien n'est ouvert à la connexion : une application qui n'appelle jamais `onUser` ne paie rien. Mesuré : 494 octets par onglet abonné, envoi ~60 ns. |
|
|
584
|
+
|
|
585
|
+
L'`id` est l'identifiant **stable** du compte (`RealtimeIdentity.id`), jamais le login : un login
|
|
586
|
+
libéré se réattribue, et le nouveau compte recevrait les messages de l'ancien. Un appelant sans
|
|
587
|
+
compte local (`<émetteur>#<sujet>`, connexion d'un fournisseur d'identité) a lui aussi son canal ;
|
|
588
|
+
un **anonyme** n'en a pas — il n'a pas d'identifiant qui survive à une reconnexion. Le support
|
|
589
|
+
anonyme passe par un salon privé de l'application (`support:<conversation>` + `allow`, ci-dessous).
|
|
590
|
+
|
|
591
|
+
**Le prix assumé** : un compte supprimé garde son abonnement jusqu'à son éjection explicite. Et une
|
|
592
|
+
publication sans abonné est perdue sans erreur — c'est une diffusion, pas une boîte aux lettres :
|
|
593
|
+
l'historique d'une conversation vit dans la base de l'application.
|
|
594
|
+
|
|
595
|
+
**Surcharger la règle** : une politique que l'application écrit sur `user:…` (nom exact ou motif)
|
|
596
|
+
l'emporte sur la règle intégrée — c'est ainsi qu'on ouvre `user:public` à tous. Une telle
|
|
597
|
+
déclaration remplace la garantie « toi seul » : à n'écrire qu'en connaissance de cause.
|
|
598
|
+
|
|
599
|
+
### 🪪 `allow` — admettre un abonné par SON identité
|
|
600
|
+
|
|
601
|
+
Les trois axes décrivent une **classe** d'identité : « un administrateur », « un service qui porte
|
|
602
|
+
`metrics:read` ». Ils ne savent pas dire « **ce** compte, et lui seul, sur **ce** canal » — la boîte
|
|
603
|
+
aux lettres `user:42`, la salle d'une partie, le tableau de bord d'une seule usine. C'est le rôle
|
|
604
|
+
d'`allow` : une règle qui reçoit le canal demandé et l'identité de l'abonné, et répond oui ou non.
|
|
605
|
+
Elle se joue au `subscribe`, une fois, dans le contrôleur
|
|
606
|
+
(`RealtimeController.ts#RealtimeController.startChannel`) ; la règle elle-même est un contrat du cœur
|
|
607
|
+
(`channelAdmission.ts#IChannelAdmission`), isomorphe : rien n'y dépend de la socket.
|
|
608
|
+
|
|
609
|
+
> L'exemple ci-dessous écrit à la main la règle de `user:*` pour montrer la mécanique. Pour le canal
|
|
610
|
+
> personnel lui-même, **ne rien écrire** : le framework le fournit (section précédente), et une
|
|
611
|
+
> politique écrite ainsi remplacerait la sienne.
|
|
612
|
+
|
|
613
|
+
**La forme courte — une flèche, pour une règle qui ne lit rien :**
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// nodefony/controllers/InboxRealtimeController.ts
|
|
617
|
+
import { controller, route } from "@nodefony/framework";
|
|
618
|
+
import { Context } from "@nodefony/http";
|
|
619
|
+
import { RealtimeChannel, RealtimeController } from "@nodefony/realtime";
|
|
620
|
+
import type { RealtimePublish } from "@nodefony/realtime";
|
|
621
|
+
|
|
622
|
+
@controller("/rt")
|
|
623
|
+
class InboxRealtimeController extends RealtimeController {
|
|
624
|
+
constructor(context: Context) {
|
|
625
|
+
super("InboxRealtimeController", context);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
@route("inbox-ws", {
|
|
629
|
+
path: "/inbox",
|
|
630
|
+
requirements: { methods: ["WEBSOCKET"] },
|
|
631
|
+
})
|
|
632
|
+
async realtime(message: string | Buffer | null): Promise<void> {
|
|
633
|
+
this.handleRealtime(message);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// La règle du MOTIF : `user:42` n'est ouvert qu'au compte 42.
|
|
637
|
+
@RealtimeChannel("user:*", {
|
|
638
|
+
allow: (channel, who) => channel === `user:${who.id}`,
|
|
639
|
+
})
|
|
640
|
+
inbox(): () => void {
|
|
641
|
+
return () => {};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Un producteur décoré ne sert que son nom exact : les canaux `user:<id>`
|
|
645
|
+
// sont servis par la fabrique dynamique.
|
|
646
|
+
override createRealtimeChannel(
|
|
647
|
+
channel: string,
|
|
648
|
+
publish: RealtimePublish,
|
|
649
|
+
): (() => void) | null {
|
|
650
|
+
if (!channel.startsWith("user:")) return null;
|
|
651
|
+
publish(channel, { unread: 0 });
|
|
652
|
+
return () => {};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export default InboxRealtimeController;
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Le compte 42 reçoit `realtime:subscribed` pour `user:42` ; le compte 43 et l'anonyme reçoivent
|
|
660
|
+
`realtime:denied`, motif `forbidden` — prouvé de bout en bout, vrai client et vrai verrou, par
|
|
661
|
+
`realtimeChannelAuth.e2e.test.ts`.
|
|
662
|
+
|
|
663
|
+
**La forme longue — une classe, dès que la règle lit les données de l'application.** Ses services lui
|
|
664
|
+
arrivent par le constructeur, comme à tout service ; elle se déclare dans `@services([…])` de son
|
|
665
|
+
module, et le décorateur la nomme :
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// nodefony/services/RoomAdmission.ts
|
|
669
|
+
import { Inject, Module, Service, injectable } from "nodefony";
|
|
670
|
+
import type { IChannelAdmission, ISubscriberIdentity } from "nodefony";
|
|
671
|
+
import type RoomMembersService from "./RoomMembersService";
|
|
672
|
+
|
|
673
|
+
@injectable()
|
|
674
|
+
class RoomAdmission extends Service implements IChannelAdmission {
|
|
675
|
+
constructor(
|
|
676
|
+
module: Module,
|
|
677
|
+
@Inject("RoomMembersService") private readonly members: RoomMembersService,
|
|
678
|
+
) {
|
|
679
|
+
super(
|
|
680
|
+
"roomAdmission",
|
|
681
|
+
module.container ?? undefined,
|
|
682
|
+
module.notificationsCenter,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// `room:<partie>` n'est ouvert qu'aux joueurs de la partie.
|
|
687
|
+
async allow(channel: string, who: ISubscriberIdentity): Promise<boolean> {
|
|
688
|
+
if (who.id === null) return false;
|
|
689
|
+
return this.members.isPlayer(channel.slice("room:".length), who.id);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export default RoomAdmission;
|
|
694
|
+
|
|
695
|
+
// Dans le contrôleur :
|
|
696
|
+
// @RealtimeChannel("room:*", { allow: RoomAdmission })
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Le démarrage résout la classe une fois et journalise « canal room:\*, admission RoomAdmission,
|
|
700
|
+
résolue » (`channelPolicyRegistry.ts#createAdmissionResolver`). Une dépendance absente, une portée
|
|
701
|
+
autre que singleton, une classe sans méthode `allow` font **échouer le démarrage** en nommant la
|
|
702
|
+
classe et le canal — jamais un refus silencieux de 100 % des abonnés en production. Une seule
|
|
703
|
+
instance sert tous les abonnés en même temps : elle ne garde **aucun état par abonné**.
|
|
704
|
+
|
|
705
|
+
Un **module npm** livre sa politique de la même façon : la classe vit dans le module, déclarée dans
|
|
706
|
+
ses `@services([…])`, son contrôleur la nomme. L'application qui l'installe n'écrit rien.
|
|
707
|
+
|
|
708
|
+
**Le contrat, en six règles :**
|
|
709
|
+
|
|
710
|
+
| Règle | Pourquoi |
|
|
711
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
712
|
+
| **Ses arguments, et rien d'autre** | ni contexte de requête, ni session : à l'émission d'un webhook il n'y en a pas. Une règle qui les lirait passerait tous ses tests sur la socket, puis refuserait tout ailleurs |
|
|
713
|
+
| **`who.id`, jamais le login** | un e-mail change, un login libéré se réattribue ; `id` est l'identifiant stable du compte, `null` pour l'anonyme |
|
|
714
|
+
| **Seul `true` admet** | `1`, `"yes"`, un objet refusent — une réponse ambiguë ne vaut pas un oui |
|
|
715
|
+
| **L'anonyme est refusé d'office** | sans même appeler la règle, sauf `anonymous: true` dans la politique ; un authentificateur qui ne fournit pas d'identité d'admission (`getIdentity`) n'entre sur aucun canal à `allow` |
|
|
716
|
+
| **Elle s'ajoute aux trois axes** | `authenticated`, `roles`, `scopes` d'abord (le verrou), puis `allow` (l'abonnement). Une politique plus spécifique remplace celle du motif EN ENTIER, `allow` compris : `@RealtimeChannel("user:public", { authenticated: false })` ouvre ce canal-là |
|
|
717
|
+
| **Une fois par abonnement, jamais par trame** | admis, les trames entrantes du canal passent sans la rappeler ; une trame entrante sur un canal à `allow` SANS abonnement préalable est refusée |
|
|
718
|
+
|
|
719
|
+
Le `realtime:welcome` annonce un canal à `allow` sur le seul verdict du verrou : la règle n'est
|
|
720
|
+
jamais jouée à la poignée de main — elle coûterait un appel par canal déclaré et par connexion, pour
|
|
721
|
+
annoncer un motif dont aucun nom concret n'est connu.
|
|
722
|
+
|
|
723
|
+
**Quand la règle est lente ou en panne** — la base qu'elle lit ne répond plus :
|
|
724
|
+
|
|
725
|
+
<!-- prettier-ignore -->
|
|
726
|
+
| Situation | L'abonné reçoit | Le serveur |
|
|
727
|
+
| --- | --- | --- |
|
|
728
|
+
| la règle lève, ou sa promesse est rejetée | `realtime:denied` `forbidden` (fermé par défaut) | un `WARNING` par fenêtre de 10 s |
|
|
729
|
+
| aucune réponse dans `allowTimeoutMs` (5 s) | `unavailable` | `allowTimedOutTotal` ; un verdict arrivé après coup est ignoré, jamais une admission |
|
|
730
|
+
| plus de `maxPendingAllowsPerConnection` (8) appels en vol sur la connexion | rien : la demande **attend son tour** | — le rejeu de tous les abonnements au `welcome` n'en perd aucun |
|
|
731
|
+
| plus de `maxPendingAllowsTotal` (512) appels en vol sur l'exemplaire | `unavailable`, sans appel | `allowSaturatedTotal` |
|
|
732
|
+
| 50 délais dépassés en 5 s | `unavailable` et `retryAfterMs`, sans appel, pendant 5 s (coupe-circuit) | `allowSaturatedTotal` |
|
|
733
|
+
|
|
734
|
+
Un délai dépassé n'est **pas** un refus de droit : le client réessaie `unavailable` au plus trois
|
|
735
|
+
fois, avec un recul exponentiel à gigue complète, après `retryAfterMs` quand il est donné.
|
|
736
|
+
`forbidden` et `limit` ne se réessaient jamais. Le tout reste borné : 300 passerelles qui rejouent
|
|
737
|
+
10 000 abonnements contre une base **bloquée** déclenchent environ 1 600 appels de la règle, jamais
|
|
738
|
+
plus de 512 à la fois, et la tempête s'éteint en moins d'une minute
|
|
739
|
+
(`admissionStorm.e2e.test.ts`). Réglages : [`configuration.md`](./configuration.md) ; compteurs :
|
|
740
|
+
[`observabilite.md`](./observabilite.md).
|
|
741
|
+
|
|
742
|
+
**Le prix assumé — une réponse retenue.** Une admission ne se périme pas seule : un membre retiré
|
|
743
|
+
d'une salle reste abonné tant que sa connexion vit, la règle n'est pas rejouée. Pour couper tout de
|
|
744
|
+
suite, on retire son appartenance (là où la règle la lit), PUIS on l'éjecte :
|
|
745
|
+
`getRealtimeHub().evictUser(canal, compte)` coupe tous ses onglets, sur tous les serveurs
|
|
746
|
+
(`realtime:closed { reason: "evicted" }`). L'éjection seule ne suffit pas — son navigateur rejoue
|
|
747
|
+
`subscribe` au rechargement, et c'est la règle qui tranche alors. Détail :
|
|
748
|
+
[canaux à membres](./canaux-a-membres.md). Fermer la connexion entière, c'est la révocation
|
|
749
|
+
([verrou 4](#-verrou-4--la-révocation--une-socket-survit-à-sa-session)).
|
|
750
|
+
|
|
751
|
+
**`room:*` et `pattern: "room:"` — deux grammaires, une même famille.** Le décorateur écrit un motif
|
|
752
|
+
(`"room:*"`) ; la configuration de `@nodefony/security` écrit un préfixe
|
|
753
|
+
(`realtimeChannels: [{ pattern: "room:", roles }]`). Ils couvrent les mêmes canaux. La règle de
|
|
754
|
+
configuration est jugée par le verrou AVANT la politique du décorateur et la remplace de ce côté —
|
|
755
|
+
c'est ainsi qu'une exploitation durcit un namespace sans toucher au code ; elle ne porte jamais
|
|
756
|
+
d'`allow`, qui reste déclaré sur le décorateur et s'applique toujours à l'abonnement.
|
|
757
|
+
|
|
542
758
|
### ⚠️ La condition d'activation — le point critique
|
|
543
759
|
|
|
544
760
|
Le verrou n'existe que si quelqu'un le pose. Deux conditions doivent être vraies **en même temps** :
|
|
@@ -623,44 +839,89 @@ ou changée → `-32000` avec `status: 401`, et le client bascule sur un `fetch`
|
|
|
623
839
|
courant. Une erreur de re-validation vaut refus (fail-closed).
|
|
624
840
|
|
|
625
841
|
**Sur les canaux**, le hub n'inscrit au registre de révocation que les connexions dont le token porte
|
|
626
|
-
`isValid` (`RealtimeController.ts#RealtimeController.onHandshake`)
|
|
842
|
+
`isValid` (`RealtimeController.ts#RealtimeController.onHandshake`). Une connexion anonyme n'y entre jamais,
|
|
843
|
+
coût nul. Toute connexion authentifiée par le pare-feu y entre : sa session est relue, ou, pour un
|
|
844
|
+
jeton porteur (`jwt`, `external-jwt`, `apikey`), son expiration (`exp`), la révocation de son `jti` et
|
|
845
|
+
la révocation en masse de son identité (`invalidBefore`).
|
|
627
846
|
`RealtimeHub.registerRevocable()` (`RealtimeHub.ts#RealtimeHub.registerRevocable`) démarre un `setInterval` `unref` au premier
|
|
628
847
|
inscrit et l'arrête dès que le registre se vide : zéro timer au repos. Période :
|
|
629
848
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts#channelFamily`), 30 s, alignée sur le heartbeat WS.
|
|
630
849
|
|
|
631
850
|
Ce que `realtimeRevocation.attack.test.ts` prouve exactement :
|
|
632
851
|
|
|
633
|
-
| Cas | Comportement prouvé
|
|
634
|
-
| ------------------------------------------ |
|
|
635
|
-
| `isValid()` renvoie `false` | `close(4001, "session revoked")`
|
|
636
|
-
| `
|
|
637
|
-
| `
|
|
638
|
-
|
|
|
639
|
-
|
|
|
640
|
-
|
|
|
852
|
+
| Cas | Comportement prouvé |
|
|
853
|
+
| ------------------------------------------ | --------------------------------------------------------- |
|
|
854
|
+
| `isValid()` renvoie `false` | `close(4001, "session revoked")` |
|
|
855
|
+
| `status()` rend `expired` (jeton porteur) | `close(4006, "token expired")` |
|
|
856
|
+
| `status()` rend `revoked` | `close(4001, "session revoked")`, même si `exp` est passé |
|
|
857
|
+
| `isValid()` renvoie `true` | socket **intacte** (contrôle positif, pas de faux refus) |
|
|
858
|
+
| `isValid()` **throw** (store indisponible) | socket fermée — fail-closed, parité avec `api.request` |
|
|
859
|
+
| déjà révoqué | retiré du registre avant le close → aucun re-close |
|
|
860
|
+
| déconnexion propre | `unregisterRevocable` → plus jamais re-validé |
|
|
861
|
+
| registre vide | tick no-op, aucun crash |
|
|
641
862
|
|
|
642
863
|
La source de vérité côté security est `buildSessionRevalidator()`
|
|
643
864
|
(`FirewallRealtimeAuthenticator.ts#buildSessionRevalidator`) : il relit la session BFF **par son id capturé au handshake**
|
|
644
865
|
et vérifie qu'elle est toujours vivante **et toujours celle du même utilisateur** — ce second point
|
|
645
866
|
attrape le changement de compte sur un navigateur partagé.
|
|
646
867
|
|
|
868
|
+
**Le code de fermeture dit la cause** (`realtime/closeCodes.ts`, ADR-0021 §S11). Un jeton porteur
|
|
869
|
+
sait pourquoi il est mort : `IRealtimeToken.status()` rend `expired` ou `revoked`, et la
|
|
870
|
+
révocation l'emporte — la liste de refus est interrogée avant de conclure à l'expiration.
|
|
871
|
+
|
|
872
|
+
| Cause sur une socket OUVERTE | Code | Client 10.0 |
|
|
873
|
+
| ------------------------------------------------------------- | -------------------------------------- | -------------------- |
|
|
874
|
+
| jeton porteur arrivé à `exp` | `4006` (`TOKEN_EXPIRED_CLOSE_CODE`) | s'arrête (définitif) |
|
|
875
|
+
| jeton porteur révoqué (`jti`, `invalidBefore`), expiré ou non | `4001` (`IDENTITY_DEAD_CLOSE_CODE`) | s'arrête |
|
|
876
|
+
| session expirée ou détruite (la cause n'est pas connaissable) | `4001` | s'arrête |
|
|
877
|
+
| droits changés, identité vivante | `4005` (`IDENTITY_REFRESH_CLOSE_CODE`) | revient aussitôt |
|
|
878
|
+
| refus à la poignée de main, quel qu'il soit | 401 uniforme → `1008` | s'arrête |
|
|
879
|
+
| authentification sans réponse dans `authenticateTimeoutMs` | `1013` (`TRY_AGAIN_LATER_CLOSE_CODE`) | revient, avec recul |
|
|
880
|
+
| client qui ne vide plus sa file (contre-pression) | `1013` | revient, avec recul |
|
|
881
|
+
|
|
882
|
+
> [!IMPORTANT]
|
|
883
|
+
> **Jamais de `4006` à la poignée de main.** Un refus distinct y dirait à qui tient un jeton volé
|
|
884
|
+
> qu'il a été émis par l'application : un jeton expiré présenté à l'entrée reçoit le même 401 que
|
|
885
|
+
> n'importe quel autre refus.
|
|
886
|
+
|
|
887
|
+
**Recette — un jeton d'une heure sur une socket de sept jours.** La socket vit plus longtemps que son
|
|
888
|
+
jeton : au bout d'une heure, elle est fermée en `4006`, et le client 10.0 ne revient pas seul. C'est
|
|
889
|
+
à l'application de **recréer sa socket avec un jeton neuf avant `exp`** — par exemple obtenir un
|
|
890
|
+
nouveau jeton cinq minutes avant l'échéance, ouvrir la nouvelle socket, puis fermer l'ancienne.
|
|
891
|
+
Un client qui saura obtenir un jeton frais de lui-même viendra dans une version 10.x, sans que le
|
|
892
|
+
serveur change.
|
|
893
|
+
|
|
894
|
+
**Un changement de rôles coupe sur-le-champ, sans attendre le tick.** Quand une mise à jour du compte
|
|
895
|
+
touche ses rôles, le hub ferme toutes les sockets de cette identité avec le code **4005**
|
|
896
|
+
(`realtime/closeCodes.ts#IDENTITY_REFRESH_CLOSE_CODE`, `RealtimeHub.ts#RealtimeHub.#revocation`). À la
|
|
897
|
+
différence de 4001 (identité morte, définitif), 4005 est **reconnectable** : le client se reconnecte
|
|
898
|
+
seul et reçoit ses nouveaux rôles dans l'accueil. Les flux SSE du même compte suivent la même règle.
|
|
899
|
+
Aucune lecture de base par tick ni par frame : le coût n'existe qu'à la mise à jour qui touche
|
|
900
|
+
`roles`.
|
|
901
|
+
|
|
647
902
|
> [!CAUTION]
|
|
648
903
|
> **Trois limites à connaître.**
|
|
649
904
|
>
|
|
650
905
|
> 1. **Fenêtre de 30 s.** Entre le logout et le tick suivant, les flux continuent. Pour une coupure
|
|
651
|
-
> immédiate, fermer la socket explicitement côté serveur.
|
|
652
|
-
>
|
|
653
|
-
>
|
|
654
|
-
> (
|
|
655
|
-
>
|
|
656
|
-
>
|
|
657
|
-
>
|
|
658
|
-
>
|
|
906
|
+
> immédiate, fermer la socket explicitement côté serveur. (Un changement de **rôles**, lui, coupe
|
|
907
|
+
> immédiatement — ci-dessus.)
|
|
908
|
+
> 2. **Une identité qu'on ne pourra pas re-valider ferme la socket.** Si la session est illisible au
|
|
909
|
+
> handshake (ni id ni store exploitable), `buildSessionRevalidator()`
|
|
910
|
+
> (`FirewallRealtimeAuthenticator.ts#buildSessionRevalidator`) rend un revalidateur qui répond
|
|
911
|
+
> toujours `false` : la socket est fermée en 4001 au premier tick, et un `WARNING` le dit. Même
|
|
912
|
+
> règle pour un jeton porteur qui n'a ni `exp`, ni de quoi interroger le store de révocation
|
|
913
|
+
> (`jti`, `iat`). Un refus visible plutôt qu'une connexion que rien ne pourrait plus couper.
|
|
914
|
+
> 3. **Un jeton porteur n'est jugé sur sa signature qu'à l'ouverture.** Le tick relit `exp`, `jti` et
|
|
915
|
+
> `invalidBefore`, jamais la clé ni l'émetteur : une clé retirée du jeu publié par l'émetteur laisse
|
|
916
|
+
> la socket vivre jusqu'à `exp`, et un émetteur retiré de `resourceServer.issuers` ne compte qu'à
|
|
917
|
+
> la connexion suivante — la liste ne change qu'au redémarrage, qui ferme les sockets. Une
|
|
918
|
+
> connexion anonyme, elle, n'est pas révocable du tout.
|
|
659
919
|
|
|
660
920
|
## 🚧 Plafonds anti-abus — borner une connexion
|
|
661
921
|
|
|
662
922
|
Une connexion ouverte est une ressource. Trois bornes existent ; il faut aussi savoir ce qui **n'est
|
|
663
|
-
pas** borné.
|
|
923
|
+
pas** borné. Les appels en vol de la règle `allow` ont leurs propres plafonds, par connexion et par
|
|
924
|
+
exemplaire : voir [`allow` — quand la règle est lente](#-allow--admettre-un-abonné-par-son-identité).
|
|
664
925
|
|
|
665
926
|
### Canaux par connexion
|
|
666
927
|
|
|
@@ -774,16 +1035,16 @@ n'est pas appliquée.
|
|
|
774
1035
|
|
|
775
1036
|
## 📜 Normes appliquées
|
|
776
1037
|
|
|
777
|
-
| Norme | Application
|
|
778
|
-
| ------------------------------------- |
|
|
779
|
-
| **RFC 6455 §10.2** (Origin) | Contrôle d'origine à l'upgrade, aux deux étages
|
|
780
|
-
| **RFC 6455 §7.4.2** (codes 4000-4999) | `4001` unauthorized / session revoked, `4003` origin not allowed
|
|
781
|
-
| **RFC 6455 §7.4.1 / registre IANA** | `1008` Policy Violation (origine transport), `1009` Message Too Big, `1013` Try Again Later
|
|
782
|
-
| **RFC 6455 §4.1** | `Sec-WebSocket-Protocol` normalisé en liste dans le DTO de handshake
|
|
783
|
-
| **RFC 9110 §7.2** | Comparaison du `Host` par le matcher d'authenticator
|
|
784
|
-
| **JSON-RPC 2.0 §5.1** | `-32001` (plage `Server error`) pour une requête refusée, `-32602` pour des params invalides
|
|
785
|
-
| **OWASP WSTG-CLNT-10** | Anti-CSWSH : validation d'`Origin` obligatoire, les navigateurs n'appliquent pas CORS aux WS
|
|
786
|
-
| **Zero Trust / OWASP A01** | Zone protégée + anonyme = refus ; token toujours présent, jamais `null`
|
|
1038
|
+
| Norme | Application |
|
|
1039
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1040
|
+
| **RFC 6455 §10.2** (Origin) | Contrôle d'origine à l'upgrade, aux deux étages |
|
|
1041
|
+
| **RFC 6455 §7.4.2** (codes 4000-4999) | `4001` unauthorized / session revoked, `4003` origin not allowed, `4006` token expired |
|
|
1042
|
+
| **RFC 6455 §7.4.1 / registre IANA** | `1008` Policy Violation (origine transport), `1009` Message Too Big, `1013` Try Again Later (contre-pression, authentification sans réponse) |
|
|
1043
|
+
| **RFC 6455 §4.1** | `Sec-WebSocket-Protocol` normalisé en liste dans le DTO de handshake |
|
|
1044
|
+
| **RFC 9110 §7.2** | Comparaison du `Host` par le matcher d'authenticator |
|
|
1045
|
+
| **JSON-RPC 2.0 §5.1** | `-32001` (plage `Server error`) pour une requête refusée, `-32602` pour des params invalides |
|
|
1046
|
+
| **OWASP WSTG-CLNT-10** | Anti-CSWSH : validation d'`Origin` obligatoire, les navigateurs n'appliquent pas CORS aux WS |
|
|
1047
|
+
| **Zero Trust / OWASP A01** | Zone protégée + anonyme = refus ; token toujours présent, jamais `null` |
|
|
787
1048
|
|
|
788
1049
|
## 📡 Observabilité — Studio
|
|
789
1050
|
|
|
@@ -812,7 +1073,7 @@ n'est pas appliquée.
|
|
|
812
1073
|
| Le client se croit abonné, ne reçoit rien | Refus d'autorisation, plafond atteint, **ou nom de canal sans producteur** (pas de réponse RPC) | Écouter `realtime:denied` : `forbidden` (droits), `limit` (borne), `unknown` (le nom ne désigne rien) |
|
|
813
1074
|
| `slowConsumer.bytes` augmenté, les frames sont toujours jetées à 4 MiB | Cette clé pilote le **comptage** de la sonde, pas les seuils de drop/close du transport | Régler `websocket.maxBackpressure` / `.backpressureCloseAfterDrops` dans `@nodefony/http` |
|
|
814
1075
|
| Deux déploiements se parlent en cross-talk | Pas de `backplane.namespace` sur un Redis mutualisé (la base Redis ne cloisonne pas le pub/sub) | Poser un `namespace` explicite par déploiement |
|
|
815
|
-
| Le fan-out cross-pod s'arrête après avoir posé un secret | `backplane.secret` différent d'un pod à l'autre : les messages sont scellés, aucun ne se vérifie | Le **même** secret sur tous les pods (`NF_REALTIME_BACKPLANE_SECRET`) ; suivre `
|
|
1076
|
+
| Le fan-out cross-pod s'arrête après avoir posé un secret | `backplane.secret` différent d'un pod à l'autre : les messages sont scellés, aucun ne se vérifie | Le **même** secret sur tous les pods (`NF_REALTIME_BACKPLANE_SECRET`) ; suivre `sealRejectedTotal` |
|
|
816
1077
|
| Un canal broadcast ne reçoit rien des autres pods | Le préfixe n'est pas déclaré côté receveur → l'entrée refuse le canal (compté) | Déclarer le préfixe (`broadcast` du controller) sur **tous** les pods, pas seulement l'émetteur |
|
|
817
1078
|
|
|
818
1079
|
## 🧪 Tests & couverture
|
package/docs/vocabulaire.md
CHANGED
|
@@ -305,7 +305,8 @@ front**. Une différence assumée : `request()` n'y est pas supporté — au-des
|
|
|
305
305
|
### `accueil` (welcome) — la première frame
|
|
306
306
|
|
|
307
307
|
La notification `realtime:welcome`, poussée par le serveur juste après le handshake. Elle annonce le
|
|
308
|
-
protocole, les canaux que ce visiteur peut obtenir, les actions de l'endpoint,
|
|
308
|
+
protocole, les canaux que ce visiteur peut obtenir, les actions de l'endpoint, l'identité résolue et, quand
|
|
309
|
+
le client en a offert un, le codec de trame retenu (`codec`, absent = JSON-RPC texte). Un client
|
|
309
310
|
attend cette frame avant de pousser quoi que ce soit.
|
|
310
311
|
|
|
311
312
|
`IRealtimeWelcome` (`RealtimeEventMap.ts#IRealtimeWelcome`).
|
|
@@ -480,7 +481,7 @@ et de ses handlers. Aucune dépendance Node — le même fichier tourne dans le
|
|
|
480
481
|
Le geste du pair sur une frame entrante : décider de sa nature, trouver le handler, l'appeler,
|
|
481
482
|
renvoyer la réponse s'il en faut une. Ce n'est pas une méthode publique — c'est un **moment**, et
|
|
482
483
|
c'est précisément là que se greffe le [verrou de frame](#verrou-de-frame--la-décision-par-frame),
|
|
483
|
-
par le crochet `beforeDispatch` (`JsonRpcPeer.ts#
|
|
484
|
+
par le crochet `beforeDispatch` (`JsonRpcPeer.ts#IJsonRpcPeerHooks.beforeDispatch`).
|
|
484
485
|
|
|
485
486
|
### `transport` — la couche octets
|
|
486
487
|
|
|
@@ -583,7 +584,7 @@ L'identifiant **unique dans le cluster** du process émetteur, joint à chaque m
|
|
|
583
584
|
du nom du pod ou de l'hôte, jamais du seul identifiant de processus : deux conteneurs peuvent tous
|
|
584
585
|
deux être le processus n° 1.
|
|
585
586
|
|
|
586
|
-
`resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`), champ `IBackplane.originId` (`
|
|
587
|
+
`resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`), champ `IBackplane.originId` (`nodefony/src/realtime/platformHealth.ts#IBackplaneInfo.kind`).
|
|
587
588
|
|
|
588
589
|
### `anti-écho` — ne pas rejouer sa propre voix
|
|
589
590
|
|
|
@@ -608,7 +609,7 @@ L'information que chaque driver déclare sur lui-même : sait-il franchir la mac
|
|
|
608
609
|
dans la carte d'identité du backplane, journalisée au démarrage et lisible dans la sonde — c'est ce
|
|
609
610
|
qui permet de vérifier en une ligne qu'un déploiement diffuse bien au-delà d'un pod.
|
|
610
611
|
|
|
611
|
-
`IBackplaneInfo` (`
|
|
612
|
+
`IBackplaneInfo` (`nodefony/src/realtime/platformHealth.ts#IBackplaneInfo`).
|
|
612
613
|
|
|
613
614
|
## 🔐 La sécurité — qui parle, qui a le droit
|
|
614
615
|
|
|
@@ -667,7 +668,7 @@ un refus a donc toujours un auteur identifiable.
|
|
|
667
668
|
Les exigences déclarées d'un canal : authentifié, rôles, scopes. Elles s'attachent au **nom** du
|
|
668
669
|
canal, pas à la méthode qui l'implémente — parce que c'est le nom que le pare-feu résout.
|
|
669
670
|
|
|
670
|
-
`IChannelPolicy` (`
|
|
671
|
+
`IChannelPolicy` (`nodefony/src/realtime/channelAdmission.ts#IChannelPolicy`), déclaration par `@RealtimeChannel`
|
|
671
672
|
(`realtimeDecorators.ts#RealtimeChannel`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts#RealtimeHub.resolveChannelPolicy`).
|
|
672
673
|
|
|
673
674
|
### `verrou de frame` — la décision par frame
|
|
@@ -712,7 +713,7 @@ Le signal émis sur les événements protocolaires qui méritent une trace : fra
|
|
|
712
713
|
refusée, méthode inconnue, erreur interne. Émis sans attente, avec le pair — ce qui permet de
|
|
713
714
|
retrouver **qui** a été refusé, pas seulement d'où venait le paquet.
|
|
714
715
|
|
|
715
|
-
`FrameAuditReason` (`JsonRpcPeer.ts#FrameAuditReason`), crochet `onFrameAudit` (`JsonRpcPeer.ts#
|
|
716
|
+
`FrameAuditReason` (`JsonRpcPeer.ts#FrameAuditReason`), crochet `onFrameAudit` (`JsonRpcPeer.ts#IJsonRpcPeerHooks.onFrameAudit`).
|
|
716
717
|
|
|
717
718
|
### `seam` — le point de greffe
|
|
718
719
|
|
|
@@ -720,8 +721,8 @@ Littéralement une **couture** : un point prévu dans une couche basse pour qu'u
|
|
|
720
721
|
greffe du comportement **sans modifier la couche basse**. Le module en expose cinq, et c'est ce qui
|
|
721
722
|
permet à la couche sécurité de se brancher sans qu'aucune ligne de realtime ne la connaisse.
|
|
722
723
|
|
|
723
|
-
Les deux du protocole sont `beforeDispatch` (`JsonRpcPeer.ts#
|
|
724
|
-
(`JsonRpcPeer.ts#
|
|
724
|
+
Les deux du protocole sont `beforeDispatch` (`JsonRpcPeer.ts#IJsonRpcPeerHooks.beforeDispatch`) et `onFrameAudit`
|
|
725
|
+
(`JsonRpcPeer.ts#IJsonRpcPeerHooks.onFrameAudit`) ; les trois du hub sont l'authenticator, le matcher et le contrôle d'origine.
|
|
725
726
|
|
|
726
727
|
→ [Sécurité](./securite.md) pour le détail de chacun.
|
|
727
728
|
|
|
@@ -751,7 +752,7 @@ L'oscilloscope du module : « voici mon état, maintenant ». Canaux et abonnés
|
|
|
751
752
|
livraisons, connexions, octets, back-pressure, carte d'identité du backplane. Lecture **pure**, sans
|
|
752
753
|
allocation sur le chemin chaud.
|
|
753
754
|
|
|
754
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) rend un `IRealtimeProbe` (`
|
|
755
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) rend un `IRealtimeProbe` (`nodefony/src/realtime/platformHealth.ts#IRealtimeProbe`).
|
|
755
756
|
|
|
756
757
|
> [!WARNING]
|
|
757
758
|
> `IRealtimeProbe` est la **forme des données**, pas une interface à implémenter avec une méthode
|
|
@@ -774,7 +775,7 @@ Une connexion dont le back-pressure dépasse le seuil de comptage. C'est une **m
|
|
|
774
775
|
sanction : le seuil de comptage se règle indépendamment des seuils d'action.
|
|
775
776
|
|
|
776
777
|
`slowConsumer.bytes` (`config.ts#slowConsumerSchema`), champ `backpressure.slowConsumers`
|
|
777
|
-
(`
|
|
778
|
+
(`nodefony/src/realtime/platformHealth.ts#IRealtimeProbe.backpressure`).
|
|
778
779
|
|
|
779
780
|
### `compteurs de canal` — mesurés à l'arrivée
|
|
780
781
|
|
|
@@ -782,7 +783,7 @@ Nombre de messages, date du dernier, débit instantané, historique glissant —
|
|
|
782
783
|
point d'arrivée des frames, donc réutilisables par n'importe quelle application.
|
|
783
784
|
|
|
784
785
|
`IChannelStats` (`IRealtimeSocket.ts#IChannelStats.method`), côté serveur `IRealtimeChannelStat`
|
|
785
|
-
(`
|
|
786
|
+
(`nodefony/src/realtime/platformHealth.ts#IRealtimeChannelStat`).
|
|
786
787
|
|
|
787
788
|
### `santé agrégée` — la vue du pod entier
|
|
788
789
|
|
|
@@ -790,7 +791,7 @@ En multi-worker, chaque process a **sa** sonde. La vue du pod est leur fusion :
|
|
|
790
791
|
maxima repris au plus haut, instances listées une à une.
|
|
791
792
|
|
|
792
793
|
`mergeClusterHealth()` (`ClusterProbeClient.ts#mergeClusterHealth`), forme `IRealtimeClusterHealth`
|
|
793
|
-
(`
|
|
794
|
+
(`nodefony/src/realtime/platformHealth.ts#IRealtimeClusterHealth`).
|
|
794
795
|
|
|
795
796
|
### `plan de données admin` — la sonde servie à Studio
|
|
796
797
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.6",
|
|
4
4
|
"nodefony": {
|
|
5
5
|
"configSchema": "./dist/nodefony/config/configSchema.js"
|
|
6
6
|
},
|
|
@@ -44,20 +44,23 @@
|
|
|
44
44
|
"esm"
|
|
45
45
|
],
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
48
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
49
|
-
"nodefony": "^10.0.0-beta.
|
|
47
|
+
"@nodefony/framework": "^10.0.0-beta.6",
|
|
48
|
+
"@nodefony/http": "^10.0.0-beta.6",
|
|
49
|
+
"nodefony": "^10.0.0-beta.6",
|
|
50
50
|
"reflect-metadata": "0.2.2",
|
|
51
51
|
"zod": "^4.6.7"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@nodefony/
|
|
55
|
-
"@nodefony/
|
|
56
|
-
"@
|
|
57
|
-
"@
|
|
58
|
-
"nodefony": "^10.0.0-beta.
|
|
54
|
+
"@nodefony/drizzle": "^10.0.0-beta.5",
|
|
55
|
+
"@nodefony/framework": "^10.0.0-beta.6",
|
|
56
|
+
"@nodefony/http": "^10.0.0-beta.6",
|
|
57
|
+
"@nodefony/orm-core": "^10.0.0-beta.5",
|
|
58
|
+
"@nodefony/security": "^10.0.0-beta.5",
|
|
59
|
+
"@types/node": "26.6.6",
|
|
60
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
61
|
+
"nodefony": "^10.0.0-beta.6",
|
|
59
62
|
"reflect-metadata": "0.2.2",
|
|
60
|
-
"vitest": "5.0
|
|
63
|
+
"vitest": "5.1.0"
|
|
61
64
|
},
|
|
62
65
|
"private": false,
|
|
63
66
|
"engines": {
|
|
@@ -65,7 +68,7 @@
|
|
|
65
68
|
},
|
|
66
69
|
"license": "Apache-2.0",
|
|
67
70
|
"dependencies": {
|
|
68
|
-
"tslib": "2.
|
|
71
|
+
"tslib": "2.9.1"
|
|
69
72
|
},
|
|
70
73
|
"files": [
|
|
71
74
|
"dist",
|