@nodefony/realtime 10.0.0-beta.1 → 10.0.0-beta.3
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 +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +17 -10
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/config/configSchema.js +19 -0
- package/dist/nodefony/config/defineModuleConfig.js +42 -15
- package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
- package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
- package/dist/nodefony/src/backplane/bootNotices.js +43 -0
- package/dist/nodefony/src/backplane/publishQueue.js +1 -4
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
- package/dist/nodefony/src/guards.js +41 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
- package/dist/nodefony/src/server/RealtimeController.js +120 -50
- package/dist/nodefony/src/server/RealtimeHub.js +29 -51
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
- package/dist/nodefony/src/server/syslogUplink.js +4 -2
- package/dist/nodefony/src/service/RealtimeService.js +3 -2
- package/dist/nodefony/testing/index.js +41 -1
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +124 -41
- package/docs/architecture.md +93 -91
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +66 -56
- package/docs/cookbook-chat.md +39 -33
- package/docs/index.md +47 -31
- package/docs/observabilite.md +21 -18
- package/docs/protocole.md +79 -65
- package/docs/securite.md +69 -65
- package/docs/vocabulaire.md +80 -79
- package/package.json +16 -12
- package/docs/couverture-bancs.md +0 -139
package/docs/protocole.md
CHANGED
|
@@ -31,7 +31,7 @@ source: "src/packages/@nodefony/realtime/docs/protocole.md"
|
|
|
31
31
|
> **frames** dont la forme est normée. Trois formes existent, et une seule règle les distingue —
|
|
32
32
|
> **la nature d'une frame se lit sur `method`, jamais sur `id`**. Cette page donne la grammaire
|
|
33
33
|
> champ par champ, les méthodes que le serveur comprend nativement, et ce qui revient exactement
|
|
34
|
-
> quand ça rate. Le moteur est `JsonRpcPeer` (`JsonRpcPeer.ts:
|
|
34
|
+
> quand ça rate. Le moteur est `JsonRpcPeer` (`JsonRpcPeer.ts:311`), écrit une fois et branché des
|
|
35
35
|
> deux côtés du fil.
|
|
36
36
|
|
|
37
37
|
📍 [Documentation](../../../../../docs/index.md) › [Realtime](index.md) › **Protocole**
|
|
@@ -52,12 +52,12 @@ cœur du protocole : il décide s'il faut répondre, à qui, et si une réponse
|
|
|
52
52
|
|
|
53
53
|
```mermaid
|
|
54
54
|
flowchart TD
|
|
55
|
-
IN["Frame entrante (JSON déjà parsé)"] --> V{"
|
|
55
|
+
IN["Frame entrante (JSON déjà parsé)"] --> V{"frame JSON-RPC 2.0 valide ?"}
|
|
56
56
|
V -->|non| INV["invalid — auditée, AUCUNE réponse"]
|
|
57
57
|
V -->|oui| M{"method présent ?"}
|
|
58
58
|
M -->|"method + id"| REQ["requête — un handler doit rendre result ou error"]
|
|
59
59
|
M -->|"method seul"| NOT["notification — pub/sub, aucune réponse"]
|
|
60
|
-
M -->|"id
|
|
60
|
+
M -->|"id + result ou error"| RES["réponse — résout une requête sortante en attente"]
|
|
61
61
|
REQ --> OUT["result · error"]
|
|
62
62
|
NOT --> HUB["subscribe · unsubscribe · canal entrant"]
|
|
63
63
|
RES --> PROM["la Promise de l'appelant"]
|
|
@@ -79,7 +79,7 @@ Deux conséquences que le reste de la page décline :
|
|
|
79
79
|
| Pair (peer) | Le moteur de protocole d'une connexion — `JsonRpcPeer`, identique client et serveur. |
|
|
80
80
|
| Requête | Frame avec `method` **et** `id` — appelle exactement une réponse. |
|
|
81
81
|
| Notification | Frame avec `method` seul — aucune réponse, jamais. |
|
|
82
|
-
| Réponse | Frame avec `id`
|
|
82
|
+
| Réponse | Frame avec `id` sans `method`, portant `result` **ou** `error` bien formé. |
|
|
83
83
|
| Dispatch | Classer une frame entrante puis la router vers son handler. |
|
|
84
84
|
| Enveloppe | Réponse qui joint des métadonnées serveur dans un champ **frère** du `result`. |
|
|
85
85
|
| Accueil, welcome | La 1ʳᵉ notification poussée par le serveur : protocole, canaux, actions, identité. |
|
|
@@ -106,7 +106,7 @@ Nodefony ne l'a pas inventée. Le calcul, mis à plat :
|
|
|
106
106
|
|
|
107
107
|
> [!NOTE]
|
|
108
108
|
> **Le batch est une propriété de la norme, pas du moteur Nodefony.** `JsonRpcPeer.receive()`
|
|
109
|
-
> (`JsonRpcPeer.ts:
|
|
109
|
+
> (`JsonRpcPeer.ts:419`) attend un **objet** portant `jsonrpc: "2.0"` ; un tableau de frames n'en
|
|
110
110
|
> porte pas et tombe donc en `invalid`. Une frame = un objet. Le multiplexage rend le batch peu
|
|
111
111
|
> utile ici : les canaux voyagent déjà dans la même connexion.
|
|
112
112
|
|
|
@@ -118,21 +118,25 @@ d'une action. La norme fournit la grammaire ; le vocabulaire est nodefonien.
|
|
|
118
118
|
|
|
119
119
|
Trois partis pris distinguent cette implémentation d'un simple « serveur WebSocket qui parle JSON ».
|
|
120
120
|
|
|
121
|
-
**Le même fichier tourne dans le navigateur et dans Node.** `JsonRpcPeer` (`JsonRpcPeer.ts:
|
|
121
|
+
**Le même fichier tourne dans le navigateur et dans Node.** `JsonRpcPeer` (`JsonRpcPeer.ts:311`)
|
|
122
122
|
n'a aucune dépendance Node — seulement `setTimeout`. Classer, router, corréler les `id` : ce travail
|
|
123
123
|
est identique des deux côtés, il est donc écrit **une seule fois**. Chaque côté l'entoure de son
|
|
124
124
|
transport et de ses handlers. Historiquement, cette discrimination vivait à deux endroits qui
|
|
125
|
-
divergeaient ; c'est précisément la classe de bug que l'isomorphisme supprime.
|
|
125
|
+
divergeaient ; c'est précisément la classe de bug que l'isomorphisme supprime. Le classement
|
|
126
|
+
lui-même est `classifyJsonRpcFrame` (`jsonrpc/index.ts:178`), la brique que partage aussi le
|
|
127
|
+
serveur MCP du framework : les deux portes ne peuvent plus diverger sur ce qu'est une frame valide.
|
|
126
128
|
|
|
127
129
|
**Le rôle se lit sur `method`, pas sur `id`.** La lecture naïve (« `id` présent = requête ») casse
|
|
128
130
|
sur les réponses, qui portent un `id` sans être des appels. `JsonRpcPeer.receive()`
|
|
129
|
-
(`JsonRpcPeer.ts:
|
|
130
|
-
|
|
131
|
+
(`JsonRpcPeer.ts:419`) teste donc `method` d'abord, puis la présence d'un `id` — chaîne ou nombre :
|
|
132
|
+
un `id` `null`, objet ou booléen rend la frame `invalid`, il n'en fait pas une notification. Une
|
|
133
|
+
frame `id` sans `method`, portant `result` ou une `error` bien formée, est une **réponse** et ne
|
|
134
|
+
déclenche jamais `-32601` à tort ; sans l'un ni l'autre, elle est `invalid`.
|
|
131
135
|
|
|
132
136
|
**Un échec ne raconte rien par défaut.** Un handler qui lève une exception ordinaire produit un
|
|
133
|
-
`-32603 "internal error"` **générique** (`JsonRpcPeer.ts:
|
|
137
|
+
`-32603 "internal error"` **générique** (`JsonRpcPeer.ts:573`) : ni message d'origine, ni pile
|
|
134
138
|
d'appels. Pour exposer volontairement un refus au pair — un 404, un droit manquant — il existe une
|
|
135
|
-
porte explicite, `RpcError` (`JsonRpcPeer.ts:
|
|
139
|
+
porte explicite, `RpcError` (`JsonRpcPeer.ts:101`), et elle seule.
|
|
136
140
|
|
|
137
141
|
## 🚀 Démarrage rapide
|
|
138
142
|
|
|
@@ -202,14 +206,14 @@ export default ChatController;
|
|
|
202
206
|
|
|
203
207
|
```ts
|
|
204
208
|
// frontend/src/chat.ts
|
|
205
|
-
import {
|
|
209
|
+
import { NodefonySocket, RpcError } from "nodefony/client";
|
|
206
210
|
|
|
207
211
|
interface ChatHistory {
|
|
208
212
|
messages: string[];
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
export async function joinChat(): Promise<void> {
|
|
212
|
-
const socket = new
|
|
216
|
+
const socket = new NodefonySocket({
|
|
213
217
|
url: "wss://127.0.0.1:5152/chat/realtime",
|
|
214
218
|
});
|
|
215
219
|
await socket.connect();
|
|
@@ -268,7 +272,7 @@ L'accueil porte cinq champs — `ts`, `protocol`, `channels`, `methods`, `identi
|
|
|
268
272
|
(`IRealtimeWelcome`, `RealtimeEventMap.ts:231`) — plus un sixième, `env`, **uniquement hors
|
|
269
273
|
production** (`welcomeEnv()`, `welcomeEnv.ts:19`) : c'est lui que porte l'exemple ci-dessus, capturé
|
|
270
274
|
en développement. Émis par le contrôleur au handshake (`peer.notify("realtime:welcome")`,
|
|
271
|
-
`RealtimeController.ts:
|
|
275
|
+
`RealtimeController.ts:796`). Il n'y a pas de champ `version`.
|
|
272
276
|
|
|
273
277
|
## 🔌 Anatomie d'une frame, champ par champ
|
|
274
278
|
|
|
@@ -285,7 +289,7 @@ en développement. Émis par le contrôleur au handshake (`peer.notify("realtime
|
|
|
285
289
|
|
|
286
290
|
| Champ | Obligatoire | Type accepté | Rôle exact |
|
|
287
291
|
| --------- | ------------------------- | -------------------- | ------------------------------------------------------------------------- |
|
|
288
|
-
| `jsonrpc` | oui, toujours | `"2.0"` littéral | Sans lui, `JsonRpcPeer.receive()` classe `invalid` (`JsonRpcPeer.ts:
|
|
292
|
+
| `jsonrpc` | oui, toujours | `"2.0"` littéral | Sans lui, `JsonRpcPeer.receive()` classe `invalid` (`JsonRpcPeer.ts:419`) |
|
|
289
293
|
| `method` | sur un appel | chaîne | Décide de la nature ET de l'aiguillage (canal, action, verbe pub/sub) |
|
|
290
294
|
| `params` | non | tout JSON | Charge applicative. Vient du réseau : **jamais** digne de confiance |
|
|
291
295
|
| `id` | sur une requête / réponse | nombre **ou** chaîne | Corrèle l'aller et le retour |
|
|
@@ -293,7 +297,7 @@ en développement. Émis par le contrôleur au handshake (`peer.notify("realtime
|
|
|
293
297
|
> [!IMPORTANT]
|
|
294
298
|
> Le pair accepte un `id` **chaîne** en entrée (conforme à la norme), mais il n'attribue jamais que
|
|
295
299
|
> des `id` **numériques** à ses propres appels sortants. Une réponse portant un `id` chaîne est donc
|
|
296
|
-
> ignorée sans erreur (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts:
|
|
300
|
+
> ignorée sans erreur (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts:579`) : elle ne peut, par
|
|
297
301
|
> construction, correspondre à aucune requête émise par ce pair.
|
|
298
302
|
|
|
299
303
|
### La réponse — `result` ou `error`, jamais les deux
|
|
@@ -312,9 +316,9 @@ distinguer un 404 d'un refus d'autorisation sans analyser un message de texte.
|
|
|
312
316
|
|
|
313
317
|
### L'enveloppe — joindre une méta sans polluer le `result`
|
|
314
318
|
|
|
315
|
-
Un handler peut rendre une `RpcEnvelope` (`JsonRpcPeer.ts:
|
|
319
|
+
Un handler peut rendre une `RpcEnvelope` (`JsonRpcPeer.ts:139`) au lieu d'une valeur nue. Le pair la
|
|
316
320
|
déballe : le `result` reste **exactement** la valeur, et la méta voyage dans un champ frère
|
|
317
|
-
`meta` (`RpcMeta`, `JsonRpcPeer.ts:
|
|
321
|
+
`meta` (`RpcMeta`, `JsonRpcPeer.ts:125`).
|
|
318
322
|
|
|
319
323
|
```jsonc
|
|
320
324
|
{
|
|
@@ -337,19 +341,20 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
|
|
|
337
341
|
|
|
338
342
|
| Méthode | Direction | `id` ? | Rôle | Ancrage |
|
|
339
343
|
| ------------------ | ------------- | :-----: | --------------------------------------------------------------------- | --------------------------- |
|
|
340
|
-
| `subscribe` | client→server | non | « pousse-moi ce canal » — `params.channel` | `RealtimeController.ts:
|
|
341
|
-
| `unsubscribe` | client→server | non | « arrête » — dernier abonné, le producteur est libéré | `RealtimeController.ts:
|
|
342
|
-
| `ping` | client→server | non | Battement de cœur — **no-op serveur**, aucun pong | `
|
|
343
|
-
| `<canal>` | server→client | non | Push d'un message : le **nom du canal est la `method`**
|
|
344
|
-
| `<canal entrant>` | client→server | non | Le client pousse sur un canal déclaré entrant | `RealtimeController.ts:
|
|
345
|
-
| `realtime:welcome` | server→client | non | L'accueil : 5 champs, dont l'identité résolue | `RealtimeController.ts:
|
|
346
|
-
| `realtime:denied` | server→client | non | Rend OBSERVABLE le refus d'une notification | `RealtimeController.ts:
|
|
347
|
-
| `api.request` | client→server | **oui** | Pont API — rejoue une route HTTP sur la socket (désactivé par défaut) | `RealtimeController.ts:
|
|
348
|
-
| `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts:
|
|
344
|
+
| `subscribe` | client→server | non | « pousse-moi ce canal » — `params.channel` | `RealtimeController.ts:577` |
|
|
345
|
+
| `unsubscribe` | client→server | non | « arrête » — dernier abonné, le producteur est libéré | `RealtimeController.ts:849` |
|
|
346
|
+
| `ping` | client→server | non | Battement de cœur — **no-op serveur**, aucun pong | `NodefonySocket.ts:1085` |
|
|
347
|
+
| `<canal>` | server→client | non | Push d'un message : le **nom du canal est la `method`** du `notify` | `RealtimeController.ts:874` |
|
|
348
|
+
| `<canal entrant>` | client→server | non | Le client pousse sur un canal déclaré entrant | `RealtimeController.ts:825` |
|
|
349
|
+
| `realtime:welcome` | server→client | non | L'accueil : 5 champs, dont l'identité résolue | `RealtimeController.ts:796` |
|
|
350
|
+
| `realtime:denied` | server→client | non | Rend OBSERVABLE le refus d'une notification | `RealtimeController.ts:573` |
|
|
351
|
+
| `api.request` | client→server | **oui** | Pont API — rejoue une route HTTP sur la socket (désactivé par défaut) | `RealtimeController.ts:651` |
|
|
352
|
+
| `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts:191` |
|
|
349
353
|
|
|
350
354
|
> [!TIP]
|
|
351
|
-
> **L'accueil est ta carte du territoire.** `
|
|
352
|
-
>
|
|
355
|
+
> **L'accueil est ta carte du territoire.** `channels` ne liste que les canaux que ce visiteur
|
|
356
|
+
> pourrait obtenir (filtrés par le même verrou que `subscribe`), `methods` les actions de l'endpoint
|
|
357
|
+
> (`RealtimeController.ts:786`) : un client peut activer ou griser
|
|
353
358
|
> ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
|
|
354
359
|
> `socket.serverChannels`.
|
|
355
360
|
|
|
@@ -359,12 +364,12 @@ Les quatre formes de frame circulent en permanence sous tes yeux — ce schéma
|
|
|
359
364
|
> `nodefony:kernel:ping` et `nodefony:kernel:gc` (`StudioRealtimeController.ts:114`) sont des exemples d'actions,
|
|
360
365
|
> **pas des méthodes du cœur temps réel** : elles sont déclarées par le contrôleur
|
|
361
366
|
> d'administration de `@nodefony/studio`. Un endpoint applicatif ne les expose pas. Le helper
|
|
362
|
-
> `
|
|
367
|
+
> `NodefonySocket.ping()` (`NodefonySocket.ts:1085`) mesure le RTT en les appelant — il suppose donc
|
|
363
368
|
> un endpoint qui les déclare, contrairement à la notification `ping` du battement de cœur, qui
|
|
364
369
|
> n'attend jamais de réponse.
|
|
365
370
|
|
|
366
371
|
`subscribe` et `unsubscribe` ne sont **pas** des actions enregistrées : elles sont traitées dans
|
|
367
|
-
`onRealtimeNotification()` (`RealtimeController.ts:
|
|
372
|
+
`onRealtimeNotification()` (`RealtimeController.ts:840`). Envoyées avec un `id`, elles seraient
|
|
368
373
|
classées « requête », ne trouveraient aucun handler et récolteraient un `-32601`.
|
|
369
374
|
|
|
370
375
|
## Une conversation type, de bout en bout
|
|
@@ -396,50 +401,59 @@ Voici **tout** ce que le moteur produit. Les codes de la plage `-32000`…`-3209
|
|
|
396
401
|
la norme aux erreurs applicatives serveur ; Nodefony y place son refus d'autorisation et le défaut
|
|
397
402
|
de `RpcError`.
|
|
398
403
|
|
|
399
|
-
| Code | Émis par
|
|
400
|
-
| -------- |
|
|
401
|
-
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:
|
|
402
|
-
| `-32603` | même méthode (`JsonRpcPeer.ts:
|
|
403
|
-
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts:
|
|
404
|
-
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts:
|
|
405
|
-
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts:
|
|
404
|
+
| Code | Émis par | Déclencheur | Ce que voit le pair |
|
|
405
|
+
| -------- | ----------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------- |
|
|
406
|
+
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:526`) | requête vers une action non enregistrée | `method not found: <nom>` |
|
|
407
|
+
| `-32603` | même méthode (`JsonRpcPeer.ts:573`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
|
|
408
|
+
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts:449`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
|
|
409
|
+
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts:101`) | le handler expose volontairement son refus | le message ET le `data` choisis par le handler |
|
|
410
|
+
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts:1027`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
|
|
406
411
|
|
|
407
412
|
Et un échec qui n'est **pas** une frame : l'expiration. `startCall()` ne reçoit rien dans le délai
|
|
408
413
|
imparti, supprime l'entrée en attente et rejette localement avec `RPC timeout: <méthode>`
|
|
409
|
-
(`JsonRpcPeer.ts:
|
|
414
|
+
(`JsonRpcPeer.ts:502`). Défaut de 30 000 ms, réglable par appel. Aucun octet ne part sur le fil : c'est une décision du
|
|
410
415
|
client, le serveur peut très bien répondre après, sa réponse sera ignorée.
|
|
411
416
|
|
|
417
|
+
L'inverse n'attend pas : une frame qui porte l'`id` d'une requête en attente sans être une réponse
|
|
418
|
+
valide — ni `result` ni `error` bien formé — fait échouer l'appel **aussitôt**, en `RpcError`
|
|
419
|
+
`-32603` « réponse JSON-RPC invalide » (`JsonRpcPeer.ts:611`). Si elle porte `result` ET une `error`
|
|
420
|
+
bien formée (la norme l'interdit), l'erreur annoncée est rendue telle quelle. Rien ne part sur le
|
|
421
|
+
fil : c'est un échec local, et la frame est auditée `invalid`.
|
|
422
|
+
|
|
412
423
|
### Les deux codes de la norme que Nodefony n'émet jamais
|
|
413
424
|
|
|
414
425
|
`-32700 Parse error` et `-32600 Invalid Request` figurent dans la norme, mais **aucune frame ne les
|
|
415
426
|
porte** ici :
|
|
416
427
|
|
|
417
428
|
- un JSON illisible n'atteint même pas le pair — le contrôleur l'abandonne à la lecture
|
|
418
|
-
(`RealtimeController.ts:
|
|
419
|
-
- une frame lisible mais non conforme
|
|
420
|
-
|
|
429
|
+
(`RealtimeController.ts:586`) ;
|
|
430
|
+
- une frame lisible mais non conforme — pas d'objet, un lot, `jsonrpc` absent ou faux, `id` `null`,
|
|
431
|
+
objet ou booléen, `method` non chaîne, réponse sans `result` ni `error` bien formé, ou avec les
|
|
432
|
+
deux — est classée `invalid`, signalée à l'audit, et **rien n'est renvoyé** (`JsonRpcPeer.ts:423`).
|
|
421
433
|
|
|
422
434
|
Le choix est délibéré : une frame cassée n'a pas d'`id` digne de confiance, donc pas de corrélation
|
|
423
435
|
possible ; et répondre systématiquement à du bruit offre à un attaquant un amplificateur gratuit.
|
|
424
|
-
Le refus reste **traçable** — c'est le motif `invalid` de `FrameAuditReason` (`JsonRpcPeer.ts:
|
|
436
|
+
Le refus reste **traçable** — c'est le motif `invalid` de `FrameAuditReason` (`JsonRpcPeer.ts:182`),
|
|
425
437
|
qui alimente le journal d'audit avec le pair concerné.
|
|
426
438
|
|
|
427
439
|
### Le refus d'une notification n'est pas une erreur
|
|
428
440
|
|
|
429
441
|
C'est la subtilité la plus importante de la page. Le verrou de frame s'applique aux requêtes **et**
|
|
430
|
-
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts:
|
|
442
|
+
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts:219`), mais leurs conséquences diffèrent
|
|
431
443
|
radicalement :
|
|
432
444
|
|
|
433
445
|
| La frame refusée est… | Ce qui part | Pourquoi |
|
|
434
446
|
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------- |
|
|
435
|
-
| une **requête** | `-32001 "unauthorized"` (`JsonRpcPeer.ts:
|
|
436
|
-
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts:
|
|
447
|
+
| une **requête** | `-32001 "unauthorized"` (`JsonRpcPeer.ts:449`) | Un `id` existe : il y a un canal de réponse |
|
|
448
|
+
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts:573`) | Aucun `id` : sans elle, le client se croirait abonné |
|
|
437
449
|
|
|
438
450
|
`IRealtimeDenied` (`RealtimeEventMap.ts:269`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
|
|
439
451
|
est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
|
|
440
|
-
attaquant y lirait la carte des droits.
|
|
441
|
-
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:
|
|
442
|
-
|
|
452
|
+
attaquant y lirait la carte des droits. Trois motifs circulent : `forbidden` (le verrou a dit non),
|
|
453
|
+
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:852`) et `unknown`
|
|
454
|
+
(aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou
|
|
455
|
+
non). Côté client,
|
|
456
|
+
`onDenied()` (`NodefonySocket.ts:678`) branche un handler dessus.
|
|
443
457
|
|
|
444
458
|
> [!CAUTION]
|
|
445
459
|
> Un `-32403 Forbidden` circule dans d'anciennes notes. **Ce code n'existe pas** dans Nodefony, et il
|
|
@@ -458,7 +472,7 @@ en découlent :
|
|
|
458
472
|
politique du canal ; la demande a exactement la même forme qu'un abonnement autorisé, seule la
|
|
459
473
|
réponse diffère.
|
|
460
474
|
3. **Le client ne pousse que là où c'est déclaré.** Un canal n'accepte d'entrée que si un handler
|
|
461
|
-
entrant existe pour ce nom (`RealtimeController.ts:
|
|
475
|
+
entrant existe pour ce nom (`RealtimeController.ts:825`) ; sinon la notification est ignorée en
|
|
462
476
|
silence. Le défaut est fermé.
|
|
463
477
|
|
|
464
478
|
Les mécanismes eux-mêmes — authenticator, verrou de frame, politique de canal, révocation — sont
|
|
@@ -466,19 +480,19 @@ décrits dans [la page sécurité](./securite.md).
|
|
|
466
480
|
|
|
467
481
|
## ⚠️ Pièges
|
|
468
482
|
|
|
469
|
-
| Symptôme | Cause | Correction
|
|
470
|
-
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
471
|
-
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts:
|
|
472
|
-
| Le handler passé à `subscribe` n'est jamais appelé | `
|
|
473
|
-
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`
|
|
474
|
-
| Un tableau de frames n'obtient aucune réponse | Le batch n'est pas implémenté : un tableau n'a pas de `jsonrpc` → `invalid` (`
|
|
475
|
-
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts:
|
|
476
|
-
| L'exception du serveur n'arrive jamais au client | Zero Trust : tout throw ordinaire devient `-32603` générique (`JsonRpcPeer.ts:
|
|
477
|
-
| Une notification refusée disparaît sans trace côté client | Sans `id`, aucune réponse possible (`beforeDispatch`, `JsonRpcPeer.ts:
|
|
478
|
-
| `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
|
|
479
|
-
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts:
|
|
480
|
-
| Une réponse reçue est ignorée sans message | Corrélation sur `id` **numériques** seulement (`JsonRpcPeer.ts:
|
|
481
|
-
| 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à
|
|
483
|
+
| Symptôme | Cause | Correction |
|
|
484
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
485
|
+
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts:809`) | L'émettre sans `id` — `socket.subscribe(canal)` |
|
|
486
|
+
| Le handler passé à `subscribe` n'est jamais appelé | `NodefonySocket.subscribe()` prend **un seul** argument (`NodefonySocket.ts:739`) | `subscribe(canal)` **et** `on(canal, handler)`, deux gestes distincts |
|
|
487
|
+
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`NodefonySocket.ts:936`) — un objet d'options n'est pas lu | `request(m, p, 5000)` ; le défaut est 30 000 ms |
|
|
488
|
+
| Un tableau de frames n'obtient aucune réponse | Le batch n'est pas implémenté : un tableau n'a pas de `jsonrpc` → `invalid` (`jsonrpc/index.ts:178`) | Une frame = un objet ; le multiplexage remplace le batch |
|
|
489
|
+
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts:423`) | Lire le motif `invalid` côté serveur, pas la réponse |
|
|
490
|
+
| L'exception du serveur n'arrive jamais au client | Zero Trust : tout throw ordinaire devient `-32603` générique (`JsonRpcPeer.ts:573`) | Lever une `RpcError` pour exposer volontairement code et `data` |
|
|
491
|
+
| Une notification refusée disparaît sans trace côté client | Sans `id`, aucune réponse possible (`beforeDispatch`, `JsonRpcPeer.ts:219`) | Écouter `realtime:denied` via `onDenied()` (`NodefonySocket.ts:678`) |
|
|
492
|
+
| `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 |
|
|
493
|
+
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts:831`) | Pour mesurer un RTT, utiliser une action RPC — `ping()` (`NodefonySocket.ts:1085`) |
|
|
494
|
+
| Une réponse reçue est ignorée sans message | Corrélation sur `id` **numériques** seulement (`JsonRpcPeer.ts:582`) | Ne pas fabriquer soi-même de réponse à `id` chaîne |
|
|
495
|
+
| 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à |
|
|
482
496
|
|
|
483
497
|
## 🧪 Tests & couverture
|
|
484
498
|
|
|
@@ -488,7 +502,7 @@ carte de l'aperçu, régénérée depuis les résultats réels — jamais figés
|
|
|
488
502
|
|
|
489
503
|
- **Le moteur, dans le cœur** (`src/nodefony/src/tests/`) : `JsonRpcPeer.test.ts` (classement des
|
|
490
504
|
frames, corrélation, timeout, erreurs), `JsonRpcPeer.types.test.ts` (le contrat générique tient à
|
|
491
|
-
la compilation), et la famille `
|
|
505
|
+
la compilation), et la famille `NodefonySocket*.test.ts` — dispatch, identité annoncée par
|
|
492
506
|
l'accueil, transport, `ping`. Ces suites **ne sont pas comptées** dans la carte du module
|
|
493
507
|
`@nodefony/realtime` : elles appartiennent au workspace `nodefony`.
|
|
494
508
|
- **Le câblage serveur, dans ce module** : `RealtimeController.test.ts` couvre les réponses
|
|
@@ -505,7 +519,7 @@ Couverture : `npm run coverage` dans `@nodefony/realtime` et dans le workspace `
|
|
|
505
519
|
> [!CAUTION]
|
|
506
520
|
> Plusieurs suites bout en bout exigent une infrastructure. **Un test sauté compte comme vert** :
|
|
507
521
|
> avant de conclure « tout passe », vérifier les variables d'environnement — la source unique est
|
|
508
|
-
> `vitest
|
|
522
|
+
> `scripts/test/vitest/gates.ts` à la racine, dont le rapport s'affiche en fin d'exécution.
|
|
509
523
|
|
|
510
524
|
## 🔗 Pour aller plus loin
|
|
511
525
|
|