@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.
Files changed (51) hide show
  1. package/README.md +29 -10
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +4 -6
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/configSchema.js +1 -2
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
  8. package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
  9. package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
  10. package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
  11. package/dist/nodefony/src/backplane/RedisBackplane.js +1 -2
  12. package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
  13. package/dist/nodefony/src/backplane/bootNotices.js +1 -2
  14. package/dist/nodefony/src/backplane/envelope.js +1 -2
  15. package/dist/nodefony/src/backplane/originId.js +1 -2
  16. package/dist/nodefony/src/backplane/publishQueue.js +0 -1
  17. package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
  18. package/dist/nodefony/src/guards.js +1 -2
  19. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
  20. package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
  21. package/dist/nodefony/src/server/RealtimeController.js +158 -84
  22. package/dist/nodefony/src/server/RealtimeHub.js +3 -3
  23. package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
  24. package/dist/nodefony/src/server/deniedDetail.js +1 -2
  25. package/dist/nodefony/src/server/subscribedAck.js +29 -0
  26. package/dist/nodefony/src/server/syslogUplink.js +1 -2
  27. package/dist/nodefony/src/server/welcomeEnv.js +1 -2
  28. package/dist/nodefony/src/service/RealtimeService.js +1 -2
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
  30. package/dist/nodefony/testing/index.js +77 -4
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
  33. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
  34. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  35. package/dist/types/nodefony/src/server/RealtimeController.d.ts +60 -2
  36. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
  37. package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
  38. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
  39. package/dist/types/nodefony/testing/index.d.ts +11 -4
  40. package/docs/actions.md +67 -44
  41. package/docs/architecture.md +125 -119
  42. package/docs/cadence-et-contre-pression.md +31 -22
  43. package/docs/configuration.md +106 -82
  44. package/docs/cookbook-chat.md +56 -34
  45. package/docs/index.md +35 -24
  46. package/docs/observabilite.md +49 -49
  47. package/docs/protocole.md +91 -68
  48. package/docs/securite.md +135 -115
  49. package/docs/vocabulaire.md +117 -114
  50. package/package.json +10 -10
  51. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/actions.md CHANGED
@@ -117,7 +117,7 @@ attendre trop longtemps et repartir.
117
117
 
118
118
  Techniquement, ce numéro est le champ `id` de la frame JSON-RPC 2.0. Le pair l'attribue, garde
119
119
  l'appel en attente dans une table, arme une minuterie, et résout la `Promise` quand la réponse
120
- portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts:579`). Une frame sans
120
+ portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts#JsonRpcPeer.handleResponse`). Une frame sans
121
121
  `id` ne crée aucune de ces trois choses — c'est pourquoi une publication ne coûte rien au repos.
122
122
 
123
123
  ## La vision Nodefony — un nom, un handler, une découverte
@@ -126,18 +126,18 @@ Trois partis pris distinguent les actions Nodefony d'une couche RPC classique.
126
126
 
127
127
  **Il n'y a pas de couche RPC séparée.** Une action est une **méthode de contrôleur**, dans la même
128
128
  classe que tes canaux et — si tu le veux — que tes routes HTTP. Pas de service dédié, pas de
129
- schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts:191`) suffit, et le
129
+ schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts#RealtimeAction`) suffit, et le
130
130
  retour de la méthode devient le `result` de la réponse.
131
131
 
132
132
  **Le contrat du handler est minuscule, volontairement.** Une action reçoit **un seul argument**,
133
133
  les paramètres bruts du client, et rend une valeur — `RpcActionHandler`
134
- (`JsonRpcPeer.ts:153`). Pas de contexte injecté dans la signature : le `this` est lié à
134
+ (`JsonRpcPeer.ts#RpcActionHandler`). Pas de contexte injecté dans la signature : le `this` est lié à
135
135
  l'instance du contrôleur au handshake, ce qui donne accès au noyau, aux services et à la
136
136
  connexion sans élargir le contrat.
137
137
 
138
138
  **L'endpoint s'annonce lui-même.** La liste des actions exposées voyage dans la frame d'accueil —
139
- `IRealtimeWelcome` (`RealtimeController.ts:12`) — et se lit côté client par
140
- `NodefonySocket.serverMethods` (`NodefonySocket.ts:800`). Une interface n'écrit donc jamais un nom d'action en dur : elle
139
+ `IRealtimeWelcome` (`RealtimeController.ts`) — et se lit côté client par
140
+ `NodefonySocket.serverMethods` (`NodefonySocket.ts#NodefonySocket.serverMethods`). Une interface n'écrit donc jamais un nom d'action en dur : elle
141
141
  n'active un bouton que si le serveur a déclaré savoir le servir.
142
142
 
143
143
  **Le compromis, dit franchement** : une action est **un aller-retour**, point. Elle ne diffuse pas,
@@ -274,20 +274,20 @@ Le trajet complet d'une requête, des étapes qu'elle traverse aux branches par
274
274
 
275
275
  ### Deux voies, un même registre
276
276
 
277
- | Voie | Quand la choisir |
278
- | ------------------------------------------------------ | ----------------------------------------------------------- |
279
- | `@RealtimeAction("nom")` (`realtimeDecorators.ts:191`) | le cas courant — un nom fixe, une méthode, déclaratif |
280
- | `realtimeActions()` (`RealtimeController.ts:281`) | la table est **calculée** (noms dynamiques, boucle, config) |
277
+ | Voie | Quand la choisir |
278
+ | -------------------------------------------------------------------------------- | ----------------------------------------------------------- |
279
+ | `@RealtimeAction("nom")` (`realtimeDecorators.ts#RealtimeAction`) | le cas courant — un nom fixe, une méthode, déclaratif |
280
+ | `realtimeActions()` (`RealtimeController.ts#RealtimeController.realtimeActions`) | la table est **calculée** (noms dynamiques, boucle, config) |
281
281
 
282
282
  Les deux sont fusionnées au handshake, et **l'override gagne** en cas de conflit de nom : une
283
283
  classe peut ainsi remplacer une action héritée sans toucher au parent
284
- (`RealtimeController.ts:598`). Un endpoint sans aucune action ne paie rien — la table du pair est
284
+ (`RealtimeController.ts#RealtimeController.onHandshake`). Un endpoint sans aucune action ne paie rien — la table du pair est
285
285
  allouée au premier enregistrement seulement.
286
286
 
287
287
  ### Ce que ton `return` et ton `throw` deviennent sur le fil
288
288
 
289
289
  Le retour du handler est envoyé tel quel en `result`. Les erreurs, elles, suivent une règle Zero
290
- Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:526`) :
290
+ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) :
291
291
 
292
292
  | Côté serveur | Ce que reçoit le client | Pourquoi |
293
293
  | ------------------------------- | ---------------------------------------- | ----------------------------------------------- |
@@ -299,7 +299,7 @@ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:52
299
299
 
300
300
  > [!WARNING]
301
301
  > **Le temps réel n'a qu'UNE erreur, et elle vient du cœur.** Pour choisir ce que voit le client,
302
- > c'est `RpcError` (`JsonRpcPeer.ts:101`), importée depuis `nodefony` — son `code` est un entier
302
+ > c'est `RpcError` (`JsonRpcPeer.ts#RpcError`), importée depuis `nodefony` — son `code` est un entier
303
303
  > JSON-RPC et son `data` traverse le fil. Toute autre exception donne un `-32603` opaque. Ne pas
304
304
  > chercher d'erreur maison dans `@nodefony/realtime` : il n'y en a pas, et
305
305
  > `tests/unit/errorSurface.test.ts` refuse d'en exporter une que personne ne lève.
@@ -313,8 +313,8 @@ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:52
313
313
 
314
314
  Cette défense est le **verrou de frame**, posé par `@nodefony/security`. Il examine chaque frame
315
315
  entrante et, pour une action, résout une politique **par son nom** — exactement le mécanisme des
316
- canaux (`buildFrameAuthorizer()`, `frameAuthorizer.ts:388` ; branche des méthodes,
317
- `frameAuthorizer.ts:388`). Trois situations, à connaître dans cet ordre :
316
+ canaux (`buildFrameAuthorizer()`, `frameAuthorizer.ts#buildFrameAuthorizer` ; branche des méthodes,
317
+ `frameAuthorizer.ts#buildFrameAuthorizer`). Trois situations, à connaître dans cet ordre :
318
318
 
319
319
  ### Situation 1 — une action est FERMÉE par défaut
320
320
 
@@ -345,22 +345,22 @@ homonyme), elle est conservée : le défaut ne rétrograde jamais une règle plu
345
345
  ### Situation 2 — les namespaces réservés portent un plancher
346
346
 
347
347
  Un nom qui commence par `nodefony:` hérite d'un plancher non contournable : authentifié **et**
348
- `ROLE_NODEFONY_ADMIN` (`SYSTEM_CHANNEL_POLICY`, `frameAuthorizer.ts:89`) — un rôle de la plateforme,
348
+ `ROLE_NODEFONY_ADMIN` (`SYSTEM_CHANNEL_POLICY`, `frameAuthorizer.ts#SYSTEM_CHANNEL_POLICY`) — un rôle de la plateforme,
349
349
  pas le `ROLE_ADMIN` d'une organisation. Un seul préfixe couvre donc tout
350
- ce que la plateforme expose (`DEFAULT_SYSTEM_PREFIXES`, `frameAuthorizer.ts:106`). C'est ce qui
350
+ ce que la plateforme expose (`DEFAULT_SYSTEM_PREFIXES`, `frameAuthorizer.ts#DEFAULT_SYSTEM_PREFIXES`). C'est ce qui
351
351
  protège `nodefony:kernel:gc` : le nom **est** la garde, et la comparaison est insensible à la casse
352
352
  pour qu'un `NODEFONY:kernel:gc` ne passe pas à côté.
353
353
 
354
354
  Ce plancher **prime sur ta déclaration** — y compris sur `{ authenticated: false }`. Et il ne
355
355
  s'arrête pas aux préfixes : tout nom **contenant** `:health` ou `:stats` est traité comme un canal
356
- d'observabilité (`matchSystemPolicy`, `frameAuthorizer.ts:243`). Une action `public:health` reste
356
+ d'observabilité (`matchSystemPolicy`, `frameAuthorizer.ts#matchSystemPolicy`). Une action `public:health` reste
357
357
  donc réservée, quoi qu'en dise son décorateur. À savoir avant de nommer une action « ouverte ».
358
358
 
359
359
  ### Situation 3 — couvrir SES actions par la configuration
360
360
 
361
361
  Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
362
362
  configuration de sécurité — la même liste que pour les canaux
363
- (`realtimeChannels`, `security/nodefony/config/config.ts:1447`) :
363
+ (`realtimeChannels`, `security/nodefony/config/config.ts#securityConfigSchema`) :
364
364
 
365
365
  ```ts ignore
366
366
  use("@nodefony/security", {
@@ -376,7 +376,7 @@ use("@nodefony/security", {
376
376
 
377
377
  Une action authentifiée sait que l'appelant est identifié ; elle peut aussi savoir **qui** il est.
378
378
  Les décorateurs de paramètres d'une route y prennent le même sens
379
- (`actionParamsWrapper`, `RealtimeController.ts:1007`) :
379
+ (`actionParamsWrapper`, `RealtimeController.ts#RealtimeController.actionParamsWrapper`) :
380
380
 
381
381
  ```ts ignore
382
382
  import { Body, CurrentUser } from "@nodefony/framework";
@@ -402,8 +402,8 @@ rien ne change pour elle, ni comportement ni coût.
402
402
 
403
403
  > [!IMPORTANT]
404
404
  > **L'identité est celle de l'ouverture de la socket.** Elle n'est pas relue à chaque appel : le
405
- > hub la revalide toutes les 30 secondes (`REVOCATION_REVALIDATE_MS`, `RealtimeHub.ts:112`) et
406
- > ferme la socket en `4001` si la session est morte (`RealtimeHub.ts:842`). Après une
405
+ > hub la revalide toutes les 30 secondes (`REVOCATION_REVALIDATE_MS`, `RealtimeHub.ts#channelFamily`) et
406
+ > ferme la socket en `4001` si la session est morte (`RealtimeHub.ts#RealtimeHub.probe`). Après une
407
407
  > déconnexion, une action peut donc encore voir l'ancien utilisateur pendant cette fenêtre. Pour
408
408
  > une écriture sensible — paiement, droits, suppression — passe par une route
409
409
  > `POST + WEBSOCKET` appelée par `socket.mutate()` : ce pont revalide l'identité à **chaque**
@@ -414,7 +414,7 @@ rien ne change pour elle, ni comportement ni coût.
414
414
  Une action réservée au développement se contrôle **dans le handler, côté serveur** — jamais par un
415
415
  drapeau d'interface. Cacher un bouton n'empêche personne de forger la frame. Le modèle est
416
416
  `nodefony:scaffold:run` dans Studio, qui refuse net hors développement avant même de lire ses paramètres
417
- (`StudioRealtimeController.ts:133`).
417
+ (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`).
418
418
 
419
419
  > [!CAUTION]
420
420
  > Le défaut fermé protège de l'oubli, pas de la sur-ouverture. Une action mutable reste la surface
@@ -428,21 +428,21 @@ drapeau d'interface. Cacher un bouton n'empêche personne de forger la frame. Le
428
428
  ### Le délai d'expiration est la seule libération automatique
429
429
 
430
430
  `request()` prend le délai en **troisième argument positionnel**, en millisecondes — il n'y a pas
431
- d'objet d'options (`NodefonySocket.request()`, `NodefonySocket.ts:936`) :
431
+ d'objet d'options (`NodefonySocket.request()`, `NodefonySocket.ts#NodefonySocket.request`) :
432
432
 
433
433
  ```ts ignore
434
434
  await socket.request("orders:quote", { orderId }); // 30 000 ms par défaut
435
435
  await socket.request("orders:export", { scope }, 120_000); // action longue
436
436
  ```
437
437
 
438
- | Défaut | Valeur | Ancrage |
439
- | --------- | --------- | -------------------- |
440
- | `request` | 30 000 ms | `JsonRpcPeer.ts:353` |
438
+ | Défaut | Valeur | Ancrage |
439
+ | --------- | --------- | ------------------------------------ |
440
+ | `request` | 30 000 ms | `JsonRpcPeer.ts#JsonRpcPeer.request` |
441
441
 
442
442
  À l'expiration, l'entrée en attente est **retirée** et la `Promise` rejetée avec
443
- `RPC timeout: <méthode>` (`JsonRpcPeer.ts:502`). Conséquence à connaître : une réponse qui
443
+ `RPC timeout: <méthode>` (`JsonRpcPeer.ts#JsonRpcPeer.startCall`). Conséquence à connaître : une réponse qui
444
444
  arriverait après coup ne trouve plus personne et est ignorée en silence
445
- (`JsonRpcPeer.ts:584`) — pas de résolution tardive, pas de fuite.
445
+ (`JsonRpcPeer.ts#JsonRpcPeer.handleResponse`) — pas de résolution tardive, pas de fuite.
446
446
 
447
447
  ### Abandonner : ce qui existe vraiment
448
448
 
@@ -456,11 +456,11 @@ Trois leviers existent, et ils ne font pas la même chose :
456
456
 
457
457
  1. **Le délai d'expiration** — libère le client, laisse le serveur travailler.
458
458
  2. **La fermeture de la connexion** — `dispose()` rejette **tous** les appels en attente d'un
459
- coup (`JsonRpcPeer.ts:480`), appelé au nettoyage de la socket
460
- (`RealtimeController.ts:749`). Là encore : côté client seulement.
459
+ coup (`JsonRpcPeer.ts#JsonRpcPeer.dispose`), appelé au nettoyage de la socket
460
+ (`RealtimeController.ts#RealtimeController.onHandshake`). Là encore : côté client seulement.
461
461
  3. **Une action compagnon** — la seule vraie annulation. On expose une seconde action qui prend
462
462
  l'identifiant du travail et l'interrompt côté serveur. Le modèle du dépôt est
463
- `nodefony:scaffold:cancel` (`StudioRealtimeController.ts:159`), pendant de `nodefony:scaffold:run`.
463
+ `nodefony:scaffold:cancel` (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`), pendant de `nodefony:scaffold:run`.
464
464
 
465
465
  ### Rejouer : idempotence par action
466
466
 
@@ -478,8 +478,8 @@ fois ? »**.
478
478
  | une mutation (`socket.mutate`) | **non** par nature | oui, **avec une clé d'idempotence** |
479
479
 
480
480
  Pour les mutations passant par le pont API, la clé n'est pas une convention : elle est **exigée
481
- par la signature** de `mutate()` (`NodefonySocket.ts:1009`), et c'est la garde `@Idempotent`
482
- (`routerDecorators.ts:1171`) qui, côté serveur, reconnaît le rejeu et rend la réponse déjà calculée
481
+ par la signature** de `mutate()` (`NodefonySocket.ts#NodefonySocket.mutate`), et c'est la garde `@Idempotent`
482
+ (`routerDecorators.ts#Idempotent`) qui, côté serveur, reconnaît le rejeu et rend la réponse déjà calculée
483
483
  au lieu de refaire l'effet.
484
484
 
485
485
  ```ts ignore
@@ -505,11 +505,11 @@ contraire des résultats **au fil de l'eau**, pendant des minutes.
505
505
  Le remède tient en trois gestes, et il est déjà en production dans Studio :
506
506
 
507
507
  1. **L'action lance et rend un identifiant**, sans attendre — `nodefony:scaffold:run`
508
- (`StudioRealtimeController.ts:133`) démarre le travail et rend son état, dont son `id`.
508
+ (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldRun`) démarre le travail et rend son état, dont son `id`.
509
509
  2. **Le flux se diffuse sur un canal dédié**, nommé d'après ce travail — `nodefony:scaffold:job@<id>`,
510
- servi par le contrôleur (`StudioRealtimeController.ts:190`).
510
+ servi par le contrôleur (`StudioRealtimeController.ts#StudioRealtimeController.actionScaffoldCancel`).
511
511
  3. **Le client s'abonne à ce canal** et regarde le travail se faire, ligne après ligne
512
- (`Create.tsx:323`).
512
+ (`Create.tsx#Create`).
513
513
 
514
514
  ### La course, et comment le dépôt la neutralise
515
515
 
@@ -519,7 +519,7 @@ prudente est donc de **s'abonner avant** de lancer l'appel.
519
519
 
520
520
  Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
521
521
  canal **rejoue son historique** au nouvel abonné, de sorte qu'un arrivant tardif voit tout depuis
522
- le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qui autorise le front
522
+ le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts#ScaffoldService.subscribe`). C'est ce qui autorise le front
523
523
  à faire l'appel d'abord et à s'abonner ensuite, sans rien perdre.
524
524
 
525
525
  > [!TIP]
@@ -529,7 +529,7 @@ le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qu
529
529
 
530
530
  ### Et le streaming du protocole ?
531
531
 
532
- **Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts:153`), que
532
+ **Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts#RpcActionHandler`), que
533
533
  le pair emballe en une réponse unique. Pour tout ce qui progresse — une réponse mot à mot d'un
534
534
  modèle de langage, un export qui avance, un traitement long — la voie est le motif
535
535
  **« travail + canal »** : l'action accuse réception, et la progression arrive sur un canal.
@@ -544,15 +544,15 @@ il sera conçu avec son premier consommateur réel.
544
544
  Un cas particulier mérite d'être connu avant d'écrire une action : **elle existe peut-être déjà en
545
545
  HTTP**. Le pont API expose la méthode `api.request`, qui rejoue une route de contrôleur sur la
546
546
  socket, avec la même garde et le même résultat qu'en REST — `invokeApiRequest()`
547
- (`RealtimeController.ts:1070`). Il est **désactivé par défaut** et s'active en surchargeant
548
- `realtimeApiRequest()` (`RealtimeController.ts:321`).
547
+ (`RealtimeController.ts#RealtimeController.invokeApiRequest`). Il est **désactivé par défaut** et s'active en surchargeant
548
+ `realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`).
549
549
 
550
550
  ```ts ignore
551
551
  const modules = await socket.request("/nodefony/kernel/api/modules");
552
552
  ```
553
553
 
554
554
  La forme se discrimine toute seule : un chemin commence par `/`, jamais un nom d'action —
555
- `NodefonySocket.request()` (`NodefonySocket.ts:936`). Écris une action RPC pour ce qui n'a de sens **que** sur la socket ;
555
+ `NodefonySocket.request()` (`NodefonySocket.ts#NodefonySocket.request`). Écris une action RPC pour ce qui n'a de sens **que** sur la socket ;
556
556
  passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit dans le
557
557
  [vocabulaire](./vocabulaire.md) et l'[architecture](./architecture.md).
558
558
 
@@ -564,6 +564,7 @@ passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit
564
564
  | Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
565
565
  | `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
566
566
  | `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
567
+ | `api.request` rejeté avec `data.status: 422` | la route invoquée valide son entrée (`schema`, ou une `ZodError` de l'action) | lire `data.fields` : chaque champ fautif, sa partie (`in`) et son message |
567
568
  | `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
568
569
  | Une action est refusée à un anonyme alors qu'on la croyait publique | une action est fermée par défaut (`{ authenticated: true }`) | déclarer `{ authenticated: false }` pour l'ouvrir ; un rôle se pose par une règle de préfixe |
569
570
  | Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |
@@ -596,11 +597,11 @@ vivent dans la carte de l'aperçu, régénérée depuis les résultats réels, j
596
597
  ### Éprouver SON action — le harnais, identité comprise
597
598
 
598
599
  Une application teste ses actions sans serveur ni navigateur avec `createRealtimeHarness`
599
- (`@nodefony/realtime/testing`, `nodefony/testing/index.ts:206`) : il monte le contrôleur sur une
600
+ (`@nodefony/realtime/testing`, `nodefony/testing/index.ts#createRealtimeHarness`) : il monte le contrôleur sur une
600
601
  fausse connexion, envoie les frames qu'un client enverrait et rend celles qui sortent. Sans option,
601
602
  la connexion est **anonyme** — le repli Zero Trust. Une action qui lit `@CurrentUser()` s'éprouve
602
603
  donc avec l'identité qu'aurait posée le pare-feu au handshake, passée en `identity`
603
- (`nodefony/testing/index.ts:93`) :
604
+ (`nodefony/testing/index.ts#IRealtimeHarnessOptions.identity`) :
604
605
 
605
606
  ```ts ignore
606
607
  import { describe, it, expect, afterEach } from "vitest";
@@ -637,7 +638,28 @@ describe("ChatController — actions", () => {
637
638
  C'est l'attribut `user` du jeton que `@CurrentUser()` rend. Pour éprouver un **refus** (`@IsGranted`,
638
639
  `policy` de canal), il faut en plus le verrou de frame de `@nodefony/security`, passé en
639
640
  `frameAuthorizer` : `@nodefony/realtime` ne connaît pas la sécurité, et sans ce verrou une règle
640
- déclarée n'est appliquée par personne — en test comme en production. La table des membres du
641
+ déclarée n'est appliquée par personne — en test comme en production.
642
+
643
+ `@nodefony/security/testing` fournit les deux pièces, et c'est la forme à préférer au jeton écrit à
644
+ la main ci-dessus : `createTestIdentity({ identifier, roles })`
645
+ (`security/nodefony/testing/index.ts#createTestIdentity`) rend l'identité authentifiée, et
646
+ `createTestFrameAuthorizer(getRealtimeHub())` (`security/nodefony/testing/index.ts#createTestFrameAuthorizer`) le **vrai**
647
+ verrou du pare-feu — même fabrique, même hiérarchie de rôles, mêmes canaux système qu'au runtime.
648
+ Un verrou factice qui ne regarde que `isAuthenticated()` laisse passer au vert un canal mal protégé.
649
+
650
+ ````ts ignore
651
+ import { getRealtimeHub } from "@nodefony/realtime";
652
+ import { createRealtimeHarness } from "@nodefony/realtime/testing";
653
+ import {
654
+ createTestFrameAuthorizer,
655
+ createTestIdentity,
656
+ } from "@nodefony/security/testing";
657
+
658
+ const h = createRealtimeHarness((ctx) => new ChatController(ctx), {
659
+ identity: createTestIdentity({ identifier: "bob", roles: ["ROLE_USER"] }),
660
+ frameAuthorizer: createTestFrameAuthorizer(getRealtimeHub()),
661
+ });
662
+ ``` La table des membres du
641
663
  harnais et l'exemple de refus sont dans le `README.md` du paquet, section « Tester un controller ».
642
664
  `nodefony create controller --kind realtime` génère ce test, identité comprise, quand l'application
643
665
  porte la sécurité.
@@ -655,3 +677,4 @@ Couverture : `npm run coverage` dans `@nodefony/realtime`.
655
677
  - La garde qui décide, côté HTTP comme WebSocket → [firewall](../../security/docs/firewall.md)
656
678
  - Les signatures exactes ne sont pas recopiées ici : elles vivent dans le graphe symbolique
657
679
  `.ai/symbols.json`, régénéré depuis les TSDoc du code.
680
+ ````