@nodefony/realtime 10.0.0-beta.3 → 10.0.0-beta.5
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 +29 -10
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +4 -6
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/configSchema.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +1 -2
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +1 -2
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +0 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
- package/dist/nodefony/src/guards.js +1 -2
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +158 -84
- package/dist/nodefony/src/server/RealtimeHub.js +3 -3
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +1 -2
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +1 -2
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +77 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +60 -2
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +11 -4
- package/docs/actions.md +67 -44
- package/docs/architecture.md +125 -119
- package/docs/cadence-et-contre-pression.md +31 -22
- package/docs/configuration.md +106 -82
- package/docs/cookbook-chat.md +56 -34
- package/docs/index.md +35 -24
- package/docs/observabilite.md +49 -49
- package/docs/protocole.md +91 -68
- package/docs/securite.md +135 -115
- package/docs/vocabulaire.md +117 -114
- package/package.json +10 -10
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
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#JsonRpcPeer`), é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**
|
|
@@ -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#JsonRpcPeer.receive`) 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,25 +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#JsonRpcPeer`)
|
|
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
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
|
|
126
|
+
lui-même est `classifyJsonRpcFrame` (`jsonrpc/index.ts#classifyJsonRpcFrame`), la brique que partage aussi le
|
|
127
127
|
serveur MCP du framework : les deux portes ne peuvent plus diverger sur ce qu'est une frame valide.
|
|
128
128
|
|
|
129
129
|
**Le rôle se lit sur `method`, pas sur `id`.** La lecture naïve (« `id` présent = requête ») casse
|
|
130
130
|
sur les réponses, qui portent un `id` sans être des appels. `JsonRpcPeer.receive()`
|
|
131
|
-
(`JsonRpcPeer.ts
|
|
131
|
+
(`JsonRpcPeer.ts#JsonRpcPeer.receive`) teste donc `method` d'abord, puis la présence d'un `id` — chaîne ou nombre :
|
|
132
132
|
un `id` `null`, objet ou booléen rend la frame `invalid`, il n'en fait pas une notification. Une
|
|
133
133
|
frame `id` sans `method`, portant `result` ou une `error` bien formée, est une **réponse** et ne
|
|
134
134
|
déclenche jamais `-32601` à tort ; sans l'un ni l'autre, elle est `invalid`.
|
|
135
135
|
|
|
136
136
|
**Un échec ne raconte rien par défaut.** Un handler qui lève une exception ordinaire produit un
|
|
137
|
-
`-32603 "internal error"` **générique** (`JsonRpcPeer.ts
|
|
137
|
+
`-32603 "internal error"` **générique** (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) : ni message d'origine, ni pile
|
|
138
138
|
d'appels. Pour exposer volontairement un refus au pair — un 404, un droit manquant — il existe une
|
|
139
|
-
porte explicite, `RpcError` (`JsonRpcPeer.ts
|
|
139
|
+
porte explicite, `RpcError` (`JsonRpcPeer.ts#RpcError`), et elle seule.
|
|
140
140
|
|
|
141
141
|
## 🚀 Démarrage rapide
|
|
142
142
|
|
|
@@ -269,10 +269,10 @@ Sur le fil, dans l'inspecteur réseau du navigateur, exactement cinq frames :
|
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
L'accueil porte cinq champs — `ts`, `protocol`, `channels`, `methods`, `identity`
|
|
272
|
-
(`IRealtimeWelcome`, `RealtimeEventMap.ts
|
|
273
|
-
production** (`welcomeEnv()`, `welcomeEnv.ts
|
|
272
|
+
(`IRealtimeWelcome`, `RealtimeEventMap.ts#IRealtimeWelcome`) — plus un sixième, `env`, **uniquement hors
|
|
273
|
+
production** (`welcomeEnv()`, `welcomeEnv.ts#welcomeEnv`) : c'est lui que porte l'exemple ci-dessus, capturé
|
|
274
274
|
en développement. Émis par le contrôleur au handshake (`peer.notify("realtime:welcome")`,
|
|
275
|
-
`RealtimeController.ts
|
|
275
|
+
`RealtimeController.ts#RealtimeController.onHandshake`). Il n'y a pas de champ `version`.
|
|
276
276
|
|
|
277
277
|
## 🔌 Anatomie d'une frame, champ par champ
|
|
278
278
|
|
|
@@ -287,17 +287,17 @@ en développement. Émis par le contrôleur au handshake (`peer.notify("realtime
|
|
|
287
287
|
}
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
| Champ | Obligatoire | Type accepté | Rôle exact
|
|
291
|
-
| --------- | ------------------------- | -------------------- |
|
|
292
|
-
| `jsonrpc` | oui, toujours | `"2.0"` littéral | Sans lui, `JsonRpcPeer.receive()` classe `invalid` (`JsonRpcPeer.ts
|
|
293
|
-
| `method` | sur un appel | chaîne | Décide de la nature ET de l'aiguillage (canal, action, verbe pub/sub)
|
|
294
|
-
| `params` | non | tout JSON | Charge applicative. Vient du réseau : **jamais** digne de confiance
|
|
295
|
-
| `id` | sur une requête / réponse | nombre **ou** chaîne | Corrèle l'aller et le retour
|
|
290
|
+
| Champ | Obligatoire | Type accepté | Rôle exact |
|
|
291
|
+
| --------- | ------------------------- | -------------------- | ----------------------------------------------------------------------------------------- |
|
|
292
|
+
| `jsonrpc` | oui, toujours | `"2.0"` littéral | Sans lui, `JsonRpcPeer.receive()` classe `invalid` (`JsonRpcPeer.ts#JsonRpcPeer.receive`) |
|
|
293
|
+
| `method` | sur un appel | chaîne | Décide de la nature ET de l'aiguillage (canal, action, verbe pub/sub) |
|
|
294
|
+
| `params` | non | tout JSON | Charge applicative. Vient du réseau : **jamais** digne de confiance |
|
|
295
|
+
| `id` | sur une requête / réponse | nombre **ou** chaîne | Corrèle l'aller et le retour |
|
|
296
296
|
|
|
297
297
|
> [!IMPORTANT]
|
|
298
298
|
> Le pair accepte un `id` **chaîne** en entrée (conforme à la norme), mais il n'attribue jamais que
|
|
299
299
|
> des `id` **numériques** à ses propres appels sortants. Une réponse portant un `id` chaîne est donc
|
|
300
|
-
> ignorée sans erreur (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts
|
|
300
|
+
> ignorée sans erreur (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts#JsonRpcPeer.handleResponse`) : elle ne peut, par
|
|
301
301
|
> construction, correspondre à aucune requête émise par ce pair.
|
|
302
302
|
|
|
303
303
|
### La réponse — `result` ou `error`, jamais les deux
|
|
@@ -316,9 +316,9 @@ distinguer un 404 d'un refus d'autorisation sans analyser un message de texte.
|
|
|
316
316
|
|
|
317
317
|
### L'enveloppe — joindre une méta sans polluer le `result`
|
|
318
318
|
|
|
319
|
-
Un handler peut rendre une `RpcEnvelope` (`JsonRpcPeer.ts
|
|
319
|
+
Un handler peut rendre une `RpcEnvelope` (`JsonRpcPeer.ts#RpcEnvelope`) au lieu d'une valeur nue. Le pair la
|
|
320
320
|
déballe : le `result` reste **exactement** la valeur, et la méta voyage dans un champ frère
|
|
321
|
-
`meta` (`RpcMeta`, `JsonRpcPeer.ts
|
|
321
|
+
`meta` (`RpcMeta`, `JsonRpcPeer.ts#RpcMeta`).
|
|
322
322
|
|
|
323
323
|
```jsonc
|
|
324
324
|
{
|
|
@@ -339,37 +339,38 @@ rétro-compatible par construction.
|
|
|
339
339
|
Ce que le serveur comprend **sans qu'aucun contrôleur ne l'ait déclaré**, plus les deux formes
|
|
340
340
|
ouvertes à l'application. Colonne `id` : présent = requête (réponse due), absent = notification.
|
|
341
341
|
|
|
342
|
-
| Méthode
|
|
343
|
-
|
|
|
344
|
-
| `subscribe`
|
|
345
|
-
| `unsubscribe`
|
|
346
|
-
| `ping`
|
|
347
|
-
| `<canal>`
|
|
348
|
-
| `<canal entrant>`
|
|
349
|
-
| `realtime:welcome`
|
|
350
|
-
| `realtime:denied`
|
|
351
|
-
| `
|
|
352
|
-
|
|
|
342
|
+
| Méthode | Direction | `id` ? | Rôle | Ancrage |
|
|
343
|
+
| --------------------- | ------------- | :-----: | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
344
|
+
| `subscribe` | client→server | non | « pousse-moi ce canal » — `params.channel` | `RealtimeController.ts#RealtimeController.onHandshake` |
|
|
345
|
+
| `unsubscribe` | client→server | non | « arrête » — dernier abonné, le producteur est libéré | `RealtimeController.ts#RealtimeController.onRealtimeNotification` |
|
|
346
|
+
| `ping` | client→server | non | Battement de cœur — **no-op serveur**, aucun pong | `NodefonySocket.ts#NodefonySocket.ping` |
|
|
347
|
+
| `<canal>` | server→client | non | Push d'un message : le **nom du canal est la `method`** du `notify` | `RealtimeController.ts#RealtimeController.startChannel` |
|
|
348
|
+
| `<canal entrant>` | client→server | non | Le client pousse sur un canal déclaré entrant | `RealtimeController.ts#RealtimeController.onRealtimeNotification` |
|
|
349
|
+
| `realtime:welcome` | server→client | non | L'accueil : 5 champs, dont l'identité résolue | `RealtimeController.ts#RealtimeController.onHandshake` |
|
|
350
|
+
| `realtime:denied` | server→client | non | Rend OBSERVABLE le refus d'une notification | `RealtimeController.ts#RealtimeController.onHandshake` |
|
|
351
|
+
| `realtime:subscribed` | server→client | non | Acquitte un `subscribe` ACCEPTÉ — le flux est actif | `RealtimeController.ts#RealtimeController.acknowledgeSubscription` |
|
|
352
|
+
| `api.request` | client→server | **oui** | Pont API — rejoue une route HTTP sur la socket (désactivé par défaut) | `RealtimeController.ts#RealtimeController.onHandshake` |
|
|
353
|
+
| `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts#RealtimeAction` |
|
|
353
354
|
|
|
354
355
|
> [!TIP]
|
|
355
356
|
> **L'accueil est ta carte du territoire.** `channels` ne liste que les canaux que ce visiteur
|
|
356
357
|
> pourrait obtenir (filtrés par le même verrou que `subscribe`), `methods` les actions de l'endpoint
|
|
357
|
-
> (`RealtimeController.ts
|
|
358
|
+
> (`RealtimeController.ts#RealtimeController.onHandshake`) : un client peut activer ou griser
|
|
358
359
|
> ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
|
|
359
360
|
> `socket.serverChannels`.
|
|
360
361
|
|
|
361
362
|
Les quatre formes de frame circulent en permanence sous tes yeux — ce schéma les montre sur la
|
|
362
363
|
|
|
363
364
|
> [!WARNING]
|
|
364
|
-
> `nodefony:kernel:ping` et `nodefony:kernel:gc` (`StudioRealtimeController.ts
|
|
365
|
+
> `nodefony:kernel:ping` et `nodefony:kernel:gc` (`StudioRealtimeController.ts#StudioRealtimeController.realtimeActions`) sont des exemples d'actions,
|
|
365
366
|
> **pas des méthodes du cœur temps réel** : elles sont déclarées par le contrôleur
|
|
366
367
|
> d'administration de `@nodefony/studio`. Un endpoint applicatif ne les expose pas. Le helper
|
|
367
|
-
> `NodefonySocket.ping()` (`NodefonySocket.ts
|
|
368
|
+
> `NodefonySocket.ping()` (`NodefonySocket.ts#NodefonySocket.ping`) mesure le RTT en les appelant — il suppose donc
|
|
368
369
|
> un endpoint qui les déclare, contrairement à la notification `ping` du battement de cœur, qui
|
|
369
370
|
> n'attend jamais de réponse.
|
|
370
371
|
|
|
371
372
|
`subscribe` et `unsubscribe` ne sont **pas** des actions enregistrées : elles sont traitées dans
|
|
372
|
-
`onRealtimeNotification()` (`RealtimeController.ts
|
|
373
|
+
`onRealtimeNotification()` (`RealtimeController.ts#RealtimeController.onRealtimeNotification`). Envoyées avec un `id`, elles seraient
|
|
373
374
|
classées « requête », ne trouveraient aucun handler et récolteraient un `-32601`.
|
|
374
375
|
|
|
375
376
|
## Une conversation type, de bout en bout
|
|
@@ -401,22 +402,25 @@ Voici **tout** ce que le moteur produit. Les codes de la plage `-32000`…`-3209
|
|
|
401
402
|
la norme aux erreurs applicatives serveur ; Nodefony y place son refus d'autorisation et le défaut
|
|
402
403
|
de `RpcError`.
|
|
403
404
|
|
|
404
|
-
| Code | Émis par
|
|
405
|
-
| -------- |
|
|
406
|
-
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts
|
|
407
|
-
| `-32603` | même méthode (`JsonRpcPeer.ts
|
|
408
|
-
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts
|
|
409
|
-
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts
|
|
410
|
-
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts
|
|
405
|
+
| Code | Émis par | Déclencheur | Ce que voit le pair |
|
|
406
|
+
| -------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- |
|
|
407
|
+
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | requête vers une action non enregistrée | `method not found: <nom>` |
|
|
408
|
+
| `-32603` | même méthode (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
|
|
409
|
+
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
|
|
410
|
+
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts#RpcError`) | le handler expose volontairement son refus | le message ET le `data` choisis par le handler |
|
|
411
|
+
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts#RealtimeController.actionParamsWrapper`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
|
|
412
|
+
| `-32000` | le pont API (`RealtimeController.ts#RealtimeController.invokeApiRequest`) | la route invoquée REFUSE : 401, 403, 404, 405, **422** | le message, `data.status` — et `data.fields` sur un 422 |
|
|
413
|
+
|
|
414
|
+
Un refus de la route invoquée par le pont garde la forme d'un `fetch` dont `ok` est faux : le statut HTTP voyage dans `data.status`. Une erreur de validation — `ZodError` levée par l'action, ou entrée refusée par le `schema` de la route — est un **422** dont `data.fields` nomme chaque champ et sa partie (`{ field, message, in }`), exactement le `error.fields` du même appel en HTTP. Tout autre `throw` reste opaque (`-32603`).
|
|
411
415
|
|
|
412
416
|
Et un échec qui n'est **pas** une frame : l'expiration. `startCall()` ne reçoit rien dans le délai
|
|
413
417
|
imparti, supprime l'entrée en attente et rejette localement avec `RPC timeout: <méthode>`
|
|
414
|
-
(`JsonRpcPeer.ts
|
|
418
|
+
(`JsonRpcPeer.ts#JsonRpcPeer.startCall`). Défaut de 30 000 ms, réglable par appel. Aucun octet ne part sur le fil : c'est une décision du
|
|
415
419
|
client, le serveur peut très bien répondre après, sa réponse sera ignorée.
|
|
416
420
|
|
|
417
421
|
L'inverse n'attend pas : une frame qui porte l'`id` d'une requête en attente sans être une réponse
|
|
418
422
|
valide — ni `result` ni `error` bien formé — fait échouer l'appel **aussitôt**, en `RpcError`
|
|
419
|
-
`-32603` « réponse JSON-RPC invalide » (`JsonRpcPeer.ts
|
|
423
|
+
`-32603` « réponse JSON-RPC invalide » (`JsonRpcPeer.ts#JsonRpcPeer.settleUnreadableResponse`). Si elle porte `result` ET une `error`
|
|
420
424
|
bien formée (la norme l'interdit), l'erreur annoncée est rendue telle quelle. Rien ne part sur le
|
|
421
425
|
fil : c'est un échec local, et la frame est auditée `invalid`.
|
|
422
426
|
|
|
@@ -426,34 +430,51 @@ fil : c'est un échec local, et la frame est auditée `invalid`.
|
|
|
426
430
|
porte** ici :
|
|
427
431
|
|
|
428
432
|
- un JSON illisible n'atteint même pas le pair — le contrôleur l'abandonne à la lecture
|
|
429
|
-
(`RealtimeController.ts
|
|
433
|
+
(`RealtimeController.ts#RealtimeController.onHandshake`) ;
|
|
430
434
|
- une frame lisible mais non conforme — pas d'objet, un lot, `jsonrpc` absent ou faux, `id` `null`,
|
|
431
435
|
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
|
|
436
|
+
deux — est classée `invalid`, signalée à l'audit, et **rien n'est renvoyé** (`JsonRpcPeer.ts#JsonRpcPeer.receive`).
|
|
433
437
|
|
|
434
438
|
Le choix est délibéré : une frame cassée n'a pas d'`id` digne de confiance, donc pas de corrélation
|
|
435
439
|
possible ; et répondre systématiquement à du bruit offre à un attaquant un amplificateur gratuit.
|
|
436
|
-
Le refus reste **traçable** — c'est le motif `invalid` de `FrameAuditReason` (`JsonRpcPeer.ts
|
|
440
|
+
Le refus reste **traçable** — c'est le motif `invalid` de `FrameAuditReason` (`JsonRpcPeer.ts#FrameAuditReason`),
|
|
437
441
|
qui alimente le journal d'audit avec le pair concerné.
|
|
438
442
|
|
|
439
443
|
### Le refus d'une notification n'est pas une erreur
|
|
440
444
|
|
|
441
445
|
C'est la subtilité la plus importante de la page. Le verrou de frame s'applique aux requêtes **et**
|
|
442
|
-
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts
|
|
446
|
+
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts#JsonRpcPeerOptions.beforeDispatch`), mais leurs conséquences diffèrent
|
|
443
447
|
radicalement :
|
|
444
448
|
|
|
445
|
-
| La frame refusée est… | Ce qui part
|
|
446
|
-
| --------------------- |
|
|
447
|
-
| une **requête** | `-32001 "unauthorized"` (`JsonRpcPeer.ts
|
|
448
|
-
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts
|
|
449
|
+
| La frame refusée est… | Ce qui part | Pourquoi |
|
|
450
|
+
| --------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
451
|
+
| une **requête** | `-32001 "unauthorized"` (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | Un `id` existe : il y a un canal de réponse |
|
|
452
|
+
| une **notification** | la notification `realtime:denied` (`RealtimeController.ts#RealtimeController.onHandshake`) | Aucun `id` : sans elle, le client se croirait abonné |
|
|
449
453
|
|
|
450
|
-
`IRealtimeDenied` (`RealtimeEventMap.ts
|
|
454
|
+
`IRealtimeDenied` (`RealtimeEventMap.ts#IRealtimeDenied`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
|
|
451
455
|
est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
|
|
452
456
|
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
|
|
457
|
+
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts#RealtimeController.onRealtimeNotification`) et `unknown`
|
|
454
458
|
(aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou
|
|
455
459
|
non). Côté client,
|
|
456
|
-
`onDenied()` (`NodefonySocket.ts
|
|
460
|
+
`onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) branche un handler dessus.
|
|
461
|
+
|
|
462
|
+
### L'abonnement accepté s'acquitte
|
|
463
|
+
|
|
464
|
+
Le pendant du refus : un `subscribe` ACCEPTÉ reçoit `realtime:subscribed { channel }`
|
|
465
|
+
(`IRealtimeSubscribed`), y compris pour un canal que la connexion suivait déjà — sans quoi un
|
|
466
|
+
`subscribe` réémis attendrait une réponse qui ne vient jamais. Un abonnement reçoit l'un OU
|
|
467
|
+
l'autre, jamais les deux.
|
|
468
|
+
|
|
469
|
+
**Garantie** : tout événement publié sur le canal APRÈS l'émission de l'acquittement est livré
|
|
470
|
+
à la connexion. Le producteur s'attache AVANT : une première charge (un tick initial) peut arriver
|
|
471
|
+
avant l'acquittement — « rien n'arrive avant » n'est pas promis. L'abonnement d'une connexion est
|
|
472
|
+
local au pod, le backplane n'y prend aucune part : l'acquittement local suffit.
|
|
473
|
+
|
|
474
|
+
Le welcome annonce la capacité (`subscriptionAcks: true`). Un client qui ne la voit pas — serveur
|
|
475
|
+
antérieur — répute l'abonnement actif dès le rejeu, au lieu d'attendre pour toujours. Côté client,
|
|
476
|
+
`whenSubscribed(canal)` se résout à l'acquittement et se rejette sur un refus du même canal ;
|
|
477
|
+
`onSubscribed()` rappelle un écouteur à chaque acquittement.
|
|
457
478
|
|
|
458
479
|
> [!CAUTION]
|
|
459
480
|
> Un `-32403 Forbidden` circule dans d'anciennes notes. **Ce code n'existe pas** dans Nodefony, et il
|
|
@@ -472,7 +493,7 @@ en découlent :
|
|
|
472
493
|
politique du canal ; la demande a exactement la même forme qu'un abonnement autorisé, seule la
|
|
473
494
|
réponse diffère.
|
|
474
495
|
3. **Le client ne pousse que là où c'est déclaré.** Un canal n'accepte d'entrée que si un handler
|
|
475
|
-
entrant existe pour ce nom (`RealtimeController.ts
|
|
496
|
+
entrant existe pour ce nom (`RealtimeController.ts#RealtimeController.onHandshake`) ; sinon la notification est ignorée en
|
|
476
497
|
silence. Le défaut est fermé.
|
|
477
498
|
|
|
478
499
|
Les mécanismes eux-mêmes — authenticator, verrou de frame, politique de canal, révocation — sont
|
|
@@ -480,19 +501,21 @@ décrits dans [la page sécurité](./securite.md).
|
|
|
480
501
|
|
|
481
502
|
## ⚠️ Pièges
|
|
482
503
|
|
|
483
|
-
| Symptôme | Cause
|
|
484
|
-
| ---------------------------------------------------------------- |
|
|
485
|
-
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts
|
|
486
|
-
| Le handler passé à `subscribe` n'est jamais appelé | `NodefonySocket.subscribe()` prend **un seul** argument (`NodefonySocket.ts
|
|
487
|
-
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`NodefonySocket.ts
|
|
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
|
|
489
|
-
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts
|
|
490
|
-
|
|
|
491
|
-
| Une
|
|
492
|
-
|
|
|
493
|
-
|
|
|
494
|
-
|
|
|
495
|
-
|
|
|
504
|
+
| Symptôme | Cause | Correction |
|
|
505
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
506
|
+
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts#RealtimeController.onHandshake`) | L'émettre sans `id` — `socket.subscribe(canal)` |
|
|
507
|
+
| Le handler passé à `subscribe` n'est jamais appelé | `NodefonySocket.subscribe()` prend **un seul** argument (`NodefonySocket.ts#NodefonySocket.subscribe`) | `subscribe(canal)` **et** `on(canal, handler)`, deux gestes distincts |
|
|
508
|
+
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`NodefonySocket.ts#NodefonySocket.request`) — un objet d'options n'est pas lu | `request(m, p, 5000)` ; le défaut est 30 000 ms |
|
|
509
|
+
| 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#classifyJsonRpcFrame`) | Une frame = un objet ; le multiplexage remplace le batch |
|
|
510
|
+
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | Lire le motif `invalid` côté serveur, pas la réponse |
|
|
511
|
+
| Une trame binaire envoyée au client n'arrive à aucun handler | Le protocole est du texte : le client l'écarte, la compte et émet une notice `-32700` (`NodefonySocket.ts#NodefonySocket.binaryFramesDiscarded`) | Un flux binaire se lit par `onBinary()` (`NodefonySocket.ts#NodefonySocket.onBinary`), hors JSON-RPC |
|
|
512
|
+
| Une trame binaire envoyée au serveur n'arrive à aucune action | Même règle côté serveur : écartée, comptée (`WsConnectionTransport.ts#WsConnectionTransport.binaryDiscarded`), journalisée une fois en WARNING | Surcharger `onRealtimeBinary()` (`RealtimeController.ts#RealtimeController.onRealtimeBinary`) ; émettre par `sendBinaryToClient()` |
|
|
513
|
+
| L'exception du serveur n'arrive jamais au client | Zero Trust : tout throw ordinaire devient `-32603` générique (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | Lever une `RpcError` pour exposer volontairement code et `data` |
|
|
514
|
+
| Une notification refusée disparaît sans trace côté client | Sans `id`, aucune réponse possible (`beforeDispatch`, `JsonRpcPeer.ts#JsonRpcPeerOptions.beforeDispatch`) | Écouter `realtime:denied` via `onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) |
|
|
515
|
+
| `nodefony:kernel:ping` répond `-32601` sur mon endpoint | L'action `nodefony:kernel:ping` est déclarée par `@nodefony/studio` (`StudioRealtimeController.ts#StudioRealtimeController.realtimeActions`) | Déclarer la sienne, ou lire `serverMethods` avant d'appeler |
|
|
516
|
+
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts#RealtimeController.onHandshake`) | Pour mesurer un RTT, utiliser une action RPC — `ping()` (`NodefonySocket.ts#NodefonySocket.ping`) |
|
|
517
|
+
| Une réponse reçue est ignorée sans message | Corrélation sur `id` **numériques** seulement (`JsonRpcPeer.ts#JsonRpcPeer.handleResponse`) | Ne pas fabriquer soi-même de réponse à `id` chaîne |
|
|
518
|
+
| 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à |
|
|
496
519
|
|
|
497
520
|
## 🧪 Tests & couverture
|
|
498
521
|
|