@nodefony/realtime 10.0.0-alpha.9 → 10.0.0-beta.2
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/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/src/backplane/envelope.js +2 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -1
- package/dist/nodefony/src/server/RealtimeController.js +52 -11
- package/dist/nodefony/src/server/RealtimeHub.js +14 -12
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +9 -7
- package/dist/nodefony/src/service/RealtimeService.js +7 -6
- package/dist/nodefony/testing/index.js +4 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +3 -3
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +27 -0
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +19 -19
- package/docs/architecture.md +35 -35
- package/docs/configuration.md +35 -30
- package/docs/cookbook-chat.md +19 -17
- package/docs/index.md +13 -14
- package/docs/observabilite.md +6 -8
- package/docs/protocole.md +24 -19
- package/docs/securite.md +43 -39
- package/docs/vocabulaire.md +23 -23
- package/package.json +10 -10
package/docs/protocole.md
CHANGED
|
@@ -246,7 +246,7 @@ Sur le fil, dans l'inspecteur réseau du navigateur, exactement cinq frames :
|
|
|
246
246
|
```jsonc
|
|
247
247
|
// 1. serveur → client : l'accueil, aussitôt après le handshake
|
|
248
248
|
{"jsonrpc":"2.0","method":"realtime:welcome","params":{
|
|
249
|
-
"ts":1770000000000,"protocol":"jsonrpc-2.0",
|
|
249
|
+
"ts":1770000000000,"protocol":"jsonrpc-2.0","env":"development",
|
|
250
250
|
"channels":["chat:room-42"],"methods":["chat:history"],
|
|
251
251
|
"identity":{"type":"anonymous","authenticated":false,
|
|
252
252
|
"userIdentifier":"anonymous","roles":["ROLE_ANONYMOUS"],"scopes":[]}}}
|
|
@@ -264,9 +264,11 @@ Sur le fil, dans l'inspecteur réseau du navigateur, exactement cinq frames :
|
|
|
264
264
|
{"jsonrpc":"2.0","method":"chat:room-42","params":{"text":"salut"}}
|
|
265
265
|
```
|
|
266
266
|
|
|
267
|
-
L'accueil porte
|
|
268
|
-
(`IRealtimeWelcome`, `RealtimeEventMap.ts:231`)
|
|
269
|
-
(`
|
|
267
|
+
L'accueil porte cinq champs — `ts`, `protocol`, `channels`, `methods`, `identity`
|
|
268
|
+
(`IRealtimeWelcome`, `RealtimeEventMap.ts:231`) — plus un sixième, `env`, **uniquement hors
|
|
269
|
+
production** (`welcomeEnv()`, `welcomeEnv.ts:19`) : c'est lui que porte l'exemple ci-dessus, capturé
|
|
270
|
+
en développement. Émis par le contrôleur au handshake (`peer.notify("realtime:welcome")`,
|
|
271
|
+
`RealtimeController.ts:708`). Il n'y a pas de champ `version`.
|
|
270
272
|
|
|
271
273
|
## 🔌 Anatomie d'une frame, champ par champ
|
|
272
274
|
|
|
@@ -335,19 +337,20 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
|
|
|
335
337
|
|
|
336
338
|
| Méthode | Direction | `id` ? | Rôle | Ancrage |
|
|
337
339
|
| ------------------ | ------------- | :-----: | --------------------------------------------------------------------- | --------------------------- |
|
|
338
|
-
| `subscribe` | client→server | non | « pousse-moi ce canal » — `params.channel` | `RealtimeController.ts:
|
|
339
|
-
| `unsubscribe` | client→server | non | « arrête » — dernier abonné, le producteur est libéré | `RealtimeController.ts:
|
|
340
|
-
| `ping` | client→server | non | Battement de cœur — **no-op serveur**, aucun pong | `RealtimeClient.ts:
|
|
341
|
-
| `<canal>` | server→client | non | Push d'un message : le **nom du canal est la `method`** | `RealtimeController.ts:
|
|
340
|
+
| `subscribe` | client→server | non | « pousse-moi ce canal » — `params.channel` | `RealtimeController.ts:522` |
|
|
341
|
+
| `unsubscribe` | client→server | non | « arrête » — dernier abonné, le producteur est libéré | `RealtimeController.ts:775` |
|
|
342
|
+
| `ping` | client→server | non | Battement de cœur — **no-op serveur**, aucun pong | `RealtimeClient.ts:882` |
|
|
343
|
+
| `<canal>` | server→client | non | Push d'un message : le **nom du canal est la `method`** | `RealtimeController.ts:972` |
|
|
342
344
|
| `<canal entrant>` | client→server | non | Le client pousse sur un canal déclaré entrant | `RealtimeController.ts:736` |
|
|
343
345
|
| `realtime:welcome` | server→client | non | L'accueil : 5 champs, dont l'identité résolue | `RealtimeController.ts:693` |
|
|
344
|
-
| `realtime:denied` | server→client | non | Rend OBSERVABLE le refus d'une notification | `RealtimeController.ts:
|
|
345
|
-
| `api.request` | client→server | **oui** | Pont API — rejoue une route HTTP sur la socket (désactivé par défaut) | `RealtimeController.ts:
|
|
346
|
+
| `realtime:denied` | server→client | non | Rend OBSERVABLE le refus d'une notification | `RealtimeController.ts:496` |
|
|
347
|
+
| `api.request` | client→server | **oui** | Pont API — rejoue une route HTTP sur la socket (désactivé par défaut) | `RealtimeController.ts:582` |
|
|
346
348
|
| `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts:101` |
|
|
347
349
|
|
|
348
350
|
> [!TIP]
|
|
349
|
-
> **L'accueil est ta carte du territoire.** `
|
|
350
|
-
>
|
|
351
|
+
> **L'accueil est ta carte du territoire.** `channels` ne liste que les canaux que ce visiteur
|
|
352
|
+
> pourrait obtenir (filtrés par le même verrou que `subscribe`), `methods` les actions de l'endpoint
|
|
353
|
+
> (`RealtimeController.ts:710`) : un client peut activer ou griser
|
|
351
354
|
> ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
|
|
352
355
|
> `socket.serverChannels`.
|
|
353
356
|
|
|
@@ -357,12 +360,12 @@ Les quatre formes de frame circulent en permanence sous tes yeux — ce schéma
|
|
|
357
360
|
> `nodefony:kernel:ping` et `nodefony:kernel:gc` (`StudioRealtimeController.ts:114`) sont des exemples d'actions,
|
|
358
361
|
> **pas des méthodes du cœur temps réel** : elles sont déclarées par le contrôleur
|
|
359
362
|
> d'administration de `@nodefony/studio`. Un endpoint applicatif ne les expose pas. Le helper
|
|
360
|
-
> `RealtimeClient.ping()` (`RealtimeClient.ts:
|
|
363
|
+
> `RealtimeClient.ping()` (`RealtimeClient.ts:878`) mesure le RTT en les appelant — il suppose donc
|
|
361
364
|
> un endpoint qui les déclare, contrairement à la notification `ping` du battement de cœur, qui
|
|
362
365
|
> n'attend jamais de réponse.
|
|
363
366
|
|
|
364
367
|
`subscribe` et `unsubscribe` ne sont **pas** des actions enregistrées : elles sont traitées dans
|
|
365
|
-
`onRealtimeNotification()` (`RealtimeController.ts:
|
|
368
|
+
`onRealtimeNotification()` (`RealtimeController.ts:784`). Envoyées avec un `id`, elles seraient
|
|
366
369
|
classées « requête », ne trouveraient aucun handler et récolteraient un `-32601`.
|
|
367
370
|
|
|
368
371
|
## Une conversation type, de bout en bout
|
|
@@ -400,7 +403,7 @@ de `RpcError`.
|
|
|
400
403
|
| `-32603` | même méthode (`JsonRpcPeer.ts:528`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
|
|
401
404
|
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts:400`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
|
|
402
405
|
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts:74`) | le handler expose volontairement son refus | le message ET le `data` choisis par le handler |
|
|
403
|
-
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts:
|
|
406
|
+
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts:787`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
|
|
404
407
|
|
|
405
408
|
Et un échec qui n'est **pas** une frame : l'expiration. `startCall()` ne reçoit rien dans le délai
|
|
406
409
|
imparti, supprime l'entrée en attente et rejette localement avec `RPC timeout: <méthode>`
|
|
@@ -431,12 +434,14 @@ radicalement :
|
|
|
431
434
|
| La frame refusée est… | Ce qui part | Pourquoi |
|
|
432
435
|
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------- |
|
|
433
436
|
| une **requête** | `-32001 "unauthorized"` (`JsonRpcPeer.ts:413`) | Un `id` existe : il y a un canal de réponse |
|
|
434
|
-
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts:
|
|
437
|
+
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts:496`) | Aucun `id` : sans elle, le client se croirait abonné |
|
|
435
438
|
|
|
436
439
|
`IRealtimeDenied` (`RealtimeEventMap.ts:269`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
|
|
437
440
|
est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
|
|
438
|
-
attaquant y lirait la carte des droits.
|
|
439
|
-
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:
|
|
441
|
+
attaquant y lirait la carte des droits. Trois motifs circulent : `forbidden` (le verrou a dit non),
|
|
442
|
+
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:811`) et `unknown`
|
|
443
|
+
(aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou
|
|
444
|
+
non). Côté client,
|
|
440
445
|
`onDenied()` (`RealtimeClient.ts:469`) branche un handler dessus.
|
|
441
446
|
|
|
442
447
|
> [!CAUTION]
|
|
@@ -474,7 +479,7 @@ décrits dans [la page sécurité](./securite.md).
|
|
|
474
479
|
| L'exception du serveur n'arrive jamais au client | Zero Trust : tout throw ordinaire devient `-32603` générique (`JsonRpcPeer.ts:528`) | Lever une `RpcError` pour exposer volontairement code et `data` |
|
|
475
480
|
| Une notification refusée disparaît sans trace côté client | Sans `id`, aucune réponse possible (`beforeDispatch`, `JsonRpcPeer.ts:151`) | Écouter `realtime:denied` via `onDenied()` (`RealtimeClient.ts:471`) |
|
|
476
481
|
| `nodefony:kernel:ping` répond `-32601` sur mon endpoint | L'action `nodefony:kernel:ping` est déclarée par `@nodefony/studio` (`StudioRealtimeController.ts:114`) | Déclarer la sienne, ou lire `serverMethods` avant d'appeler |
|
|
477
|
-
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts:
|
|
482
|
+
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts:791`) | Pour mesurer un RTT, utiliser une action RPC — `ping()` (`RealtimeClient.ts:878`) |
|
|
478
483
|
| Une réponse reçue est ignorée sans message | Corrélation sur `id` **numériques** seulement (`JsonRpcPeer.ts:537`) | Ne pas fabriquer soi-même de réponse à `id` chaîne |
|
|
479
484
|
| Les premières frames envoyées après `connect()` semblent perdues | Le transport n'est branché qu'une fois le handshake terminé | Attendre `realtime:welcome` — le client le fait déjà |
|
|
480
485
|
|
package/docs/securite.md
CHANGED
|
@@ -93,16 +93,16 @@ Les étapes 1 et 2 appartiennent à `@nodefony/http` et `@nodefony/security` : l
|
|
|
93
93
|
Une WebSocket authentifiée est une cible de choix : elle porte une identité, vit longtemps et
|
|
94
94
|
diffuse en continu.
|
|
95
95
|
|
|
96
|
-
| Attaque | Ce qui la bloque
|
|
97
|
-
| ------------------------------------------------------------------------------------------------------ |
|
|
98
|
-
| **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte
|
|
99
|
-
| **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`)
|
|
100
|
-
| **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles
|
|
101
|
-
| **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}`
|
|
102
|
-
| **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001`
|
|
103
|
-
| **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut)
|
|
104
|
-
| **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames
|
|
105
|
-
| **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail
|
|
96
|
+
| Attaque | Ce qui la bloque | Où |
|
|
97
|
+
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
98
|
+
| **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte | `@nodefony/http` transport, puis `csrf.checkOrigin` realtime |
|
|
99
|
+
| **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`) | Verrou de frame `@nodefony/security` |
|
|
100
|
+
| **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles | `@RealtimeChannel(name, { roles })` + verrou de frame |
|
|
101
|
+
| **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}` | Verrou de frame, surface `api.request` |
|
|
102
|
+
| **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001` | Tick de révocation du hub |
|
|
103
|
+
| **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut) | `limits.maxChannelsPerConnection` |
|
|
104
|
+
| **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames au-delà de 4 MiB, fermeture `1013` après 1000 jets d'affilée | Back-pressure du transport WS |
|
|
105
|
+
| **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail | `realtime:denied` |
|
|
106
106
|
|
|
107
107
|
> [!IMPORTANT]
|
|
108
108
|
> Les lignes 2 à 5 supposent `@nodefony/security` chargé **avec au moins une zone protégée**. Sans
|
|
@@ -118,7 +118,7 @@ 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:
|
|
121
|
+
(`firewall.ts:719`), et la **même** hiérarchie de rôles, `Firewall.hasRole()`
|
|
122
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`
|
|
@@ -261,13 +261,13 @@ Nodefony pose **deux barrières successives**, à deux étages différents.
|
|
|
261
261
|
| Allowlist stricte | `@nodefony/realtime` | **inactif** (`enabled: false`) | configurable | `close 4003` |
|
|
262
262
|
|
|
263
263
|
**Barrière 1 — transport, active sans rien faire.** `HttpKernel.checkWebsocketOrigin()`
|
|
264
|
-
(`http-kernel.ts:
|
|
264
|
+
(`http-kernel.ts:621`) 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
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:
|
|
270
|
+
(`RealtimeHub.ts:986`) consulte une garde compilée une fois au boot par `buildOriginGuard()`
|
|
271
271
|
(`RealtimeService.ts:313`) depuis `checkOriginSchema` (`realtime/nodefony/config/config.ts:162`).
|
|
272
272
|
Trois différences comptent :
|
|
273
273
|
|
|
@@ -292,18 +292,18 @@ n'est jamais traitée.
|
|
|
292
292
|
|
|
293
293
|
### Le pipeline exact
|
|
294
294
|
|
|
295
|
-
`RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
295
|
+
`RealtimeController.onHandshake()` (`RealtimeController.ts:374`) 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:1198`) — 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:961`) :
|
|
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).
|
|
305
305
|
5. Pose du token sur la WeakMap `peer → token` via `RealtimeHub.setTokenForPeer()`
|
|
306
|
-
(`RealtimeHub.ts:
|
|
306
|
+
(`RealtimeHub.ts:1018`), **avant** l'envoi du `welcome` : le lookup est garanti dès la première
|
|
307
307
|
frame.
|
|
308
308
|
|
|
309
309
|
Un `throw` de `authenticate()` ferme la socket en `4001` « unauthorized », après un log `WARNING`
|
|
@@ -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:943`) associe un sélecteur à une stratégie.
|
|
316
|
+
`compileMatcher()` (`RealtimeHub.ts:1418`) 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.
|
|
@@ -321,7 +321,7 @@ d'audit défectueux ne peut pas empêcher la fermeture.
|
|
|
321
321
|
- `host` optionnel → comparaison **stricte** (insensible à la casse) sur l'en-tête `Host`, sans
|
|
322
322
|
wildcard.
|
|
323
323
|
- Le match porte sur le **path**, query comprise, jamais sur l'URL absolue : `handshakePath()`
|
|
324
|
-
(`RealtimeController.ts:
|
|
324
|
+
(`RealtimeController.ts:1250`) extrait `pathname + search` du `WebsocketContext.url`, qui est
|
|
325
325
|
absolu. Sans cette extraction, un matcher `^/nodefony/…` ne se déclencherait jamais.
|
|
326
326
|
|
|
327
327
|
`@nodefony/security` enregistre ces matchers automatiquement dans `Firewall.#wireRealtime()`
|
|
@@ -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:1030`) 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 ? ».
|
|
@@ -451,7 +451,7 @@ peut **resserrer**, jamais desserrer.
|
|
|
451
451
|
|
|
452
452
|
| Cible | Politique par défaut |
|
|
453
453
|
| ------------------------------------------ | -------------------------------------------------------------- |
|
|
454
|
-
| `nodefony:audit` | authentifié + `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts:
|
|
454
|
+
| `nodefony:audit` | authentifié + `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts:120`) |
|
|
455
455
|
| tout le reste de `nodefony:` | authentifié + `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts:89`) |
|
|
456
456
|
| tout canal contenant `:health` ou `:stats` | authentifié + `ROLE_NODEFONY_ADMIN` (`frameAuthorizer.ts:253`) |
|
|
457
457
|
| tout le reste | libre, sauf policy déclarée |
|
|
@@ -472,7 +472,7 @@ Trois durcissements méritent d'être connus :
|
|
|
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:1297`) : 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é.
|
|
@@ -517,12 +517,16 @@ politique déclarée, alors que ce module en déclare pour d'autres canaux. […
|
|
|
517
517
|
|
|
518
518
|
Deux façons de fermer, selon l'intention :
|
|
519
519
|
|
|
520
|
-
- **
|
|
521
|
-
|
|
520
|
+
- **la famille doit être gardée** → déclarer un **motif** : `@RealtimeChannel("chat:room:*", { roles })`
|
|
521
|
+
garde tous les canaux de la famille ; l'avertissement propose le motif à écrire et se tait dès
|
|
522
|
+
qu'un motif couvre le canal ;
|
|
523
|
+
- **le canal doit être gardé seul** → l'exposer sous un nom déclaré (un canal par nom exact), ou
|
|
524
|
+
porter la vérification **dans la fabrique** elle-même, qui reçoit le nom demandé et peut refuser
|
|
525
|
+
(`null`) ;
|
|
522
526
|
- **le canal est public** → rien à faire, l'avertissement dit seulement qu'il n'est pas gardé.
|
|
523
527
|
|
|
524
|
-
>
|
|
525
|
-
>
|
|
528
|
+
> Le nom exact l'emporte sur un motif ; entre deux motifs, le plus spécifique gagne. Un `*` dans un
|
|
529
|
+
> nom souscrit est refusé : il désigne un motif de politique, jamais un canal.
|
|
526
530
|
|
|
527
531
|
### ⚠️ La condition d'activation — le point critique
|
|
528
532
|
|
|
@@ -536,15 +540,15 @@ C'est exactement le test de `Firewall.#wireRealtime()` (`firewall.ts:279`) : san
|
|
|
536
540
|
évaluée — ni métier, ni système. `nodefony:syslog` redevient un canal ordinaire.
|
|
537
541
|
|
|
538
542
|
Deuxième subtilité : `beforeDispatch` n'est branché sur une connexion que si le verrou est **déjà**
|
|
539
|
-
posé au moment de son handshake (`RealtimeController.ts:
|
|
540
|
-
`RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:
|
|
543
|
+
posé au moment de son handshake (`RealtimeController.ts:504`, via
|
|
544
|
+
`RealtimeHub.hasFrameAuthorizer()` — `RealtimeHub.ts:1058`). Choix de performance délibéré (un hub
|
|
541
545
|
non sécurisé garde un coût nul par frame), mais avec une conséquence : **une connexion ouverte avant
|
|
542
546
|
la pose du verrou n'est jamais gardée**, et ce jusqu'à sa fermeture. En fonctionnement normal le
|
|
543
547
|
firewall se construit au boot, avant tout trafic ; le cas ne se présente qu'en appelant
|
|
544
548
|
`setFrameAuthorizer()` à chaud.
|
|
545
549
|
|
|
546
550
|
**Le refus de dégrader en silence.** Quand des policies sont déclarées sans décideur câblé,
|
|
547
|
-
`RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:
|
|
551
|
+
`RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) renvoie `true` et le controller
|
|
548
552
|
émet un WARNING explicite, une seule fois par process (`RealtimeController.ts:558`) :
|
|
549
553
|
|
|
550
554
|
```text
|
|
@@ -600,16 +604,16 @@ Nodefony ferme l'écart par deux mécanismes de granularité différente.
|
|
|
600
604
|
|
|
601
605
|
| Surface | Re-validation | Fenêtre d'exposition | Où |
|
|
602
606
|
| --------------------------- | -------------------- | -------------------- | --------------------------------------------------------------------- |
|
|
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:
|
|
607
|
+
| `api.request` (data plane) | **à chaque frame** | nulle | `RealtimeController.invokeApiRequest()` (`RealtimeController.ts:949`) |
|
|
608
|
+
| `subscribe` / flux de canal | **périodique**, 30 s | ≤ 30 s | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:826`) |
|
|
605
609
|
|
|
606
610
|
**Sur `api.request`**, `token.isValid()` est appelé avant l'exécution de l'action ; identité périmée
|
|
607
611
|
ou changée → `-32000` avec `status: 401`, et le client bascule sur un `fetch` HTTP porteur du cookie
|
|
608
612
|
courant. Une erreur de re-validation vaut refus (fail-closed).
|
|
609
613
|
|
|
610
614
|
**Sur les canaux**, le hub n'inscrit au registre de révocation que les connexions dont le token porte
|
|
611
|
-
`isValid` (`RealtimeController.ts:
|
|
612
|
-
`RealtimeHub.registerRevocable()` (`RealtimeHub.ts:
|
|
615
|
+
`isValid` (`RealtimeController.ts:659`) — anonymes et JWT n'y entrent jamais, coût nul.
|
|
616
|
+
`RealtimeHub.registerRevocable()` (`RealtimeHub.ts:794`) démarre un `setInterval` `unref` au premier
|
|
613
617
|
inscrit et l'arrête dès que le registre se vide : zéro timer au repos. Période :
|
|
614
618
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`), 30 s, alignée sur le heartbeat WS.
|
|
615
619
|
|
|
@@ -652,7 +656,7 @@ pas** borné.
|
|
|
652
656
|
Chaque canal ouvert coûte un provider, un ticker et une entrée de Map. Sans borne, une connexion
|
|
653
657
|
peut abonner jusqu'à l'OOM — un déni de service mémoire déclenché par **un seul** client.
|
|
654
658
|
|
|
655
|
-
`RealtimeController.startChannel()` (`RealtimeController.ts:
|
|
659
|
+
`RealtimeController.startChannel()` (`RealtimeController.ts:815`) refuse au-delà de
|
|
656
660
|
`limits.maxChannelsPerConnection` (`realtime/nodefony/config/config.ts:142`), défaut **256**,
|
|
657
661
|
`null` pour illimité. Points prouvés par `realtimeChannelCap.attack.test.ts` :
|
|
658
662
|
|
|
@@ -676,7 +680,7 @@ canaux). `WsConnectionTransport.send()` (`WsConnectionTransport.ts:82`) applique
|
|
|
676
680
|
|
|
677
681
|
| `bufferedAmount` | Action |
|
|
678
682
|
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
679
|
-
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:
|
|
683
|
+
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1048`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
|
|
680
684
|
| `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
|
|
681
685
|
|
|
682
686
|
### Taille des messages entrants
|
|
@@ -693,7 +697,7 @@ au défaut de la librairie `ws`.
|
|
|
693
697
|
| Limite de **fréquence** des frames entrantes | Un client authentifié peut inonder le peer ; seul le coût CPU le freine |
|
|
694
698
|
| Limite de **connexions par IP ou par utilisateur** | Rien n'empêche N sockets par client au niveau du module |
|
|
695
699
|
| Plafond **global** de canaux du process | Le plafond est par connexion ; M connexions × 256 canaux reste possible |
|
|
696
|
-
| Seuils de back-pressure
|
|
700
|
+
| Seuils de back-pressure dans ce module | aucun réglage dans `@nodefony/realtime` : ils se règlent sur le serveur WebSocket de `@nodefony/http` (`websocket.maxBackpressure`, `.backpressureCloseAfterDrops`) |
|
|
697
701
|
|
|
698
702
|
## ⚙️ Configuration de sécurité
|
|
699
703
|
|
|
@@ -773,7 +777,7 @@ n'est pas appliquée.
|
|
|
773
777
|
## 📡 Observabilité — Studio
|
|
774
778
|
|
|
775
779
|
- **Santé de la socket** : `/nodefony/realtime/api/health`, alimenté par `RealtimeHub.probe()`
|
|
776
|
-
(`RealtimeHub.ts:
|
|
780
|
+
(`RealtimeHub.ts:865`) — canaux, abonnés, fan-out, connexions, back-pressure et carte d'identité
|
|
777
781
|
du backplane. Un `drops` qui grimpe signale des clients en souffrance ; un `slowConsumers` non nul
|
|
778
782
|
précède souvent une fermeture `1013`. Même snapshot en flux sur le canal `nodefony:socket`
|
|
779
783
|
(namespace réservé : `ROLE_NODEFONY_ADMIN`).
|
|
@@ -795,7 +799,7 @@ n'est pas appliquée.
|
|
|
795
799
|
| Un admin déconnecté garde ses flux pendant une dizaine de secondes | Le tick de révocation est périodique (30 s), pas immédiat | Comportement attendu ; pour une coupure immédiate, fermer la socket côté serveur |
|
|
796
800
|
| Une session révoquée ne ferme **jamais** la socket | La session n'était pas lisible au handshake → revalidateur `null`, `isValid()` répond toujours `true` | Vérifier que le handshake traverse bien la zone (session chargée avant le controller realtime) |
|
|
797
801
|
| 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) |
|
|
798
|
-
| `slowConsumer.bytes` augmenté, les frames sont toujours jetées à
|
|
802
|
+
| `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` |
|
|
799
803
|
| 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 |
|
|
800
804
|
| 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 `ingressRejectedTotal` |
|
|
801
805
|
| 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 |
|
package/docs/vocabulaire.md
CHANGED
|
@@ -31,7 +31,7 @@ source: "src/packages/@nodefony/realtime/docs/vocabulaire.md"
|
|
|
31
31
|
# Vocabulaire — le lexique de la socket Nodefony
|
|
32
32
|
|
|
33
33
|
> Cette page est le **dictionnaire** du temps réel Nodefony : chaque mot y a une définition, une
|
|
34
|
-
> raison d'être ce mot-là, et le symbole du code qui l'incarne. Les
|
|
34
|
+
> raison d'être ce mot-là, et le symbole du code qui l'incarne. Les huit autres pages du module
|
|
35
35
|
> l'emploient sans le redéfinir. Règle de lecture : quand une discussion realtime s'embrouille,
|
|
36
36
|
> demande-toi **de quoi on parle — de la prise, de l'autocom, ou du fond de panier ?** Neuf fois
|
|
37
37
|
> sur dix, la confusion tenait à un seul mot.
|
|
@@ -127,7 +127,7 @@ vers un autre service — et le consommateur ne voit jamais la différence
|
|
|
127
127
|
**`contrôleur` est le même mot qu'en HTTP.** Un endpoint temps réel est un contrôleur qui étend
|
|
128
128
|
`RealtimeController` (`RealtimeController.ts:144`) et porte une route WebSocket : HTTP et WebSocket
|
|
129
129
|
vivent dans le **même contexte de contrôleur**. C'est ce qui permet au pont `api.request`
|
|
130
|
-
(`RealtimeController.realtimeApiRequest()`, `RealtimeController.ts:
|
|
130
|
+
(`RealtimeController.realtimeApiRequest()`, `RealtimeController.ts:276`) de rejouer sur la socket
|
|
131
131
|
**la même action** que celle servie en REST.
|
|
132
132
|
|
|
133
133
|
## 🚀 Démarrage rapide — les mots en situation
|
|
@@ -270,7 +270,7 @@ Un handler qui lève une erreur rend un `-32603` **générique** ; seule une `Rp
|
|
|
270
270
|
|
|
271
271
|
Un canal où le **client a le droit d'émettre** vers le serveur. Défaut sûr : **aucun**. Un canal
|
|
272
272
|
n'accepte d'entrée que déclaré explicitement, par `@RealtimeInbound` (`realtimeDecorators.ts:231`) ou
|
|
273
|
-
par l'override `realtimeInbound()` (`RealtimeController.ts:
|
|
273
|
+
par l'override `realtimeInbound()` (`RealtimeController.ts:262`).
|
|
274
274
|
|
|
275
275
|
Le handler reçoit `(params, reply)` — `params` vient du réseau, donc **jamais fiable** :
|
|
276
276
|
`RealtimeInboundHandler` (`IRealtimeController.ts:16`).
|
|
@@ -297,12 +297,12 @@ 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:329`).
|
|
301
301
|
|
|
302
302
|
### `accueil` (welcome) — la première frame
|
|
303
303
|
|
|
304
304
|
La notification `realtime:welcome`, poussée par le serveur juste après le handshake. Elle annonce le
|
|
305
|
-
protocole, les canaux
|
|
305
|
+
protocole, les canaux que ce visiteur peut obtenir, les actions 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
308
|
`IRealtimeWelcome` (`RealtimeEventMap.ts:231`).
|
|
@@ -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:1451`). 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
|
|
@@ -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:1297`).
|
|
399
399
|
|
|
400
400
|
### `canal broadcast` — celui qui traverse le backplane
|
|
401
401
|
|
|
@@ -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:299`). 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
|
|
@@ -437,8 +437,8 @@ L'option qui expose la méthode `api.request { path }` : la connexion rejoue **l
|
|
|
437
437
|
contrôleur** que celle servie en REST, avec **la même garde**. Le pont n'atteint que les routes qui
|
|
438
438
|
déclarent explicitement le transport WebSocket — aucun contournement possible.
|
|
439
439
|
|
|
440
|
-
`realtimeApiRequest()` (`RealtimeController.ts:
|
|
441
|
-
(`RealtimeController.ts:
|
|
440
|
+
`realtimeApiRequest()` (`RealtimeController.ts:276`), mise en œuvre `invokeApiRequest()`
|
|
441
|
+
(`RealtimeController.ts:949`). Désactivé par défaut.
|
|
442
442
|
|
|
443
443
|
## 🔌 Le protocole et le transport — ce qui passe sur le fil
|
|
444
444
|
|
|
@@ -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:374`).
|
|
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:943`).
|
|
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:961`).
|
|
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:1228`).
|
|
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:1039`), appel
|
|
676
|
+
`runAuthorizer()` (`RealtimeHub.ts:1328`).
|
|
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:794`), `revalidateRevocable()` (`RealtimeHub.ts:826`), période
|
|
693
693
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`). Une re-validation en erreur **ferme** — jamais
|
|
694
694
|
l'inverse.
|
|
695
695
|
|
|
@@ -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:995`).
|
|
732
732
|
|
|
733
733
|
## 📡 L'observabilité — ce que la socket dit d'elle-même
|
|
734
734
|
|
|
@@ -747,7 +747,7 @@ L'oscilloscope du module : « voici mon état, maintenant ». Canaux et abonnés
|
|
|
747
747
|
livraisons, connexions, octets, back-pressure, carte d'identité du backplane. Lecture **pure**, sans
|
|
748
748
|
allocation sur le chemin chaud.
|
|
749
749
|
|
|
750
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
750
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts:865`) rend un `IRealtimeProbe` (`IRealtimeProbe.ts:61`).
|
|
751
751
|
|
|
752
752
|
> [!WARNING]
|
|
753
753
|
> `IRealtimeProbe` est la **forme des données**, pas une interface à implémenter avec une méthode
|
|
@@ -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:1048`).
|
|
766
766
|
|
|
767
767
|
### `consommateur lent` — la connexion qui décroche
|
|
768
768
|
|
|
@@ -807,7 +807,7 @@ 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:1117`) |
|
|
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
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 |
|
|
@@ -829,8 +829,8 @@ l'aperçu, régénérée depuis vitest et jamais figée ici :
|
|
|
829
829
|
- **intégration** : le driver Redis contre un serveur réel ;
|
|
830
830
|
- **bout en bout** : boucle locale, chemins de contrôleur, autorisation de canal, câblage du
|
|
831
831
|
pare-feu, cluster par communication inter-process, cluster Redis ;
|
|
832
|
-
- **attaque** : plafond de canaux, révocation, politique non appliquée
|
|
833
|
-
du lexique ont chacun leur banc hostile.
|
|
832
|
+
- **attaque** : plafond de canaux, révocation, politique non appliquée, injection par le bus — les
|
|
833
|
+
pièges de sécurité du lexique ont chacun leur banc hostile.
|
|
834
834
|
|
|
835
835
|
Couverture : `npm run coverage` dans `@nodefony/realtime`.
|
|
836
836
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
4
4
|
"description": "Couche temps réel serveur de Nodefony : canaux, appels bidirectionnels JSON-RPC 2.0, contre-pression, et diffusion entre plusieurs instances (boucle locale, IPC de grappe, Redis)",
|
|
5
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
@@ -41,20 +41,20 @@
|
|
|
41
41
|
"esm"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@nodefony/framework": "^10.0.0-
|
|
45
|
-
"@nodefony/http": "^10.0.0-
|
|
46
|
-
"nodefony": "^10.0.0-
|
|
44
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
45
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
46
|
+
"nodefony": "^10.0.0-beta.2",
|
|
47
47
|
"reflect-metadata": "0.2.2",
|
|
48
48
|
"zod": "^4.6.5"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@nodefony/framework": "^10.0.0-
|
|
52
|
-
"@nodefony/http": "^10.0.0-
|
|
53
|
-
"@types/node": "26.6.
|
|
54
|
-
"@vitest/coverage-v8": "5.0.
|
|
55
|
-
"nodefony": "^10.0.0-
|
|
51
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
52
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
53
|
+
"@types/node": "26.6.3",
|
|
54
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
55
|
+
"nodefony": "^10.0.0-beta.2",
|
|
56
56
|
"reflect-metadata": "0.2.2",
|
|
57
|
-
"vitest": "5.0.
|
|
57
|
+
"vitest": "5.0.3"
|
|
58
58
|
},
|
|
59
59
|
"private": false,
|
|
60
60
|
"engines": {
|