@nodefony/realtime 10.0.0-beta.6 → 10.0.0-beta.7

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 (62) hide show
  1. package/README.md +5 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +15 -3
  4. package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
  5. package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
  6. package/dist/nodefony/src/backplane/controlOrder.js +34 -5
  7. package/dist/nodefony/src/backplane/envelope.js +422 -45
  8. package/dist/nodefony/src/backplane/ingress.js +93 -0
  9. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  10. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  11. package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
  12. package/dist/nodefony/src/server/RealtimeController.js +87 -44
  13. package/dist/nodefony/src/server/RealtimeHub.js +631 -127
  14. package/dist/nodefony/src/server/admissionBudget.js +8 -8
  15. package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
  16. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  17. package/dist/nodefony/src/server/channelPresence.js +488 -0
  18. package/dist/nodefony/src/server/contractRoutes.js +3 -13
  19. package/dist/nodefony/src/server/frameAdmission.js +27 -35
  20. package/dist/nodefony/src/server/presenceBus.js +486 -0
  21. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  22. package/dist/nodefony/src/server/remotePresence.js +541 -0
  23. package/dist/nodefony/src/server/subscribedAck.js +3 -0
  24. package/dist/nodefony/src/service/RealtimeService.js +9 -0
  25. package/dist/nodefony/testing/backplaneConformance.js +79 -3
  26. package/dist/nodefony/testing/index.js +22 -8
  27. package/dist/nodefony/testing/presenceClient.js +54 -0
  28. package/dist/types/index.d.ts +6 -1
  29. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
  30. package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
  31. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
  32. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
  33. package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
  34. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  35. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  36. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  37. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
  38. package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
  39. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
  40. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
  41. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  42. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  43. package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
  44. package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
  45. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  46. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  47. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  48. package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
  49. package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
  50. package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
  51. package/dist/types/nodefony/testing/index.d.ts +1 -1
  52. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  53. package/docs/actions.md +1 -0
  54. package/docs/architecture.md +21 -6
  55. package/docs/canaux-a-membres.md +13 -3
  56. package/docs/cas-d-usage.md +4 -3
  57. package/docs/configuration.md +38 -2
  58. package/docs/cookbook-chat.md +400 -152
  59. package/docs/observabilite.md +28 -9
  60. package/docs/protocole.md +10 -10
  61. package/docs/securite.md +64 -10
  62. package/package.json +12 -12
@@ -261,14 +261,15 @@ Le contrat est `IRealtimeProbe` (`nodefony/src/realtime/platformHealth.ts#IRealt
261
261
 
262
262
  ### Fan-out — l'amplification réelle du broker
263
263
 
264
- | Champ | Ce qu'il dit |
265
- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
266
- | `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
267
- | `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
268
- | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts#RealtimeHub.recordInbound`). |
269
- | `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
270
- | `sealRejectedTotal` | Messages venus du **backplane** et jetés parce que leur sceau ne se vérifie pas. `0` sans bus scellé. |
271
- | `localOnlyTotal` | Publications restées sur ce serveur alors qu'un bus est branché (canal non diffusable). `0` sans bus. |
264
+ | Champ | Ce qu'il dit |
265
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
266
+ | `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
267
+ | `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
268
+ | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts#RealtimeHub.recordInbound`). |
269
+ | `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
270
+ | `sealRejectedTotal` | Messages venus du **backplane** et jetés parce que leur sceau ne se vérifie pas. `0` sans bus scellé. |
271
+ | `ingressUnknownDiscardedTotal` | Messages venus du **backplane** et jetés parce que d'une version **plus récente** : canal `nodefony:bus:*` ou type d'ordre inconnus. |
272
+ | `localOnlyTotal` | Publications restées sur ce serveur alors qu'un bus est branché (canal non diffusable). `0` sans bus. |
272
273
 
273
274
  Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
274
275
  moyenne d'un salon**. S'il vaut 1, chaque publication ne touche qu'une personne — un canal par client,
@@ -298,6 +299,21 @@ lectures possibles quand il grimpe :
298
299
  Le canal visé n'est délibérément pas exposé : un compteur d'observabilité ne doit pas devenir un
299
300
  moyen de sonder ce que le système accepte.
300
301
 
302
+ ### Messages du bus d'une version plus récente — compté à part
303
+
304
+ Une version suivante peut ajouter des messages au bus : un canal du territoire `nodefony:bus:`, ou un
305
+ nouveau type d'ordre sur le canal de contrôle. Pendant un déploiement progressif, un serveur resté sur
306
+ l'ancienne version les reçoit sans les connaître. Il les **jette** — aucun abonné n'en reçoit rien —
307
+ et les compte dans `ingressUnknownDiscardedTotal`, **jamais** dans `ingressRejectedTotal` : sinon
308
+ chaque déploiement déclencherait de fausses alertes d'injection, et une vraie attaque passerait
309
+ inaperçue dans ce bruit. Le journal le signale une seule fois par motif, en `INFO`, sans recopier le
310
+ nom reçu.
311
+
312
+ Il monte pendant un déploiement, puis s'arrête quand tous les serveurs portent la même version. S'il
313
+ continue de monter ensuite, un serveur est resté sur une version plus ancienne. Ce qui reste
314
+ suspect reste compté comme une injection : un autre canal de la plateforme, une variante de casse du
315
+ territoire, un ordre d'un type connu mais de forme fausse, ou qui vise un canal de la plateforme.
316
+
301
317
  Un message **mal scellé** n'entre pas dans ce compteur : le transport l'écarte avant le hub, et le
302
318
  compte à part dans `sealRejectedTotal` (relayé au même niveau de la sonde, et dans
303
319
  `backplane.sealRejectedTotal`). Quand celui-là grimpe, trois lectures : une rotation du secret en
@@ -409,7 +425,10 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
409
425
  signale une perte **volontaire** : la mémoire du processus a été préservée au prix de messages non
410
426
  relayés aux autres répliques. Il doit rester à zéro ; s'il bouge, regarder l'état du bus avant de
411
427
  relever le plafond (`backplane.maxQueueBytes`) — un plafond plus haut ne rattrape pas un bus en
412
- retard, il repousse l'échéance.
428
+ retard, il repousse l'échéance. Les **ordres** (terminer un canal, éjecter un compte) ne sont
429
+ jamais abandonnés pour faire place : ils passent hors plafond. Un ordre que le bus refuse (bus
430
+ injoignable) est compté à part, `controlLostTotal`, et annoncé une fois par épisode — s'il bouge,
431
+ une éjection n'a pas été appliquée par les autres répliques.
413
432
 
414
433
  ### La couche d'identité, au-dessus
415
434
 
package/docs/protocole.md CHANGED
@@ -484,16 +484,16 @@ Voici **tout** ce que le moteur produit. Les codes de la plage `-32000`…`-3209
484
484
  la norme aux erreurs applicatives serveur ; Nodefony y place son refus d'autorisation et le défaut
485
485
  de `RpcError`.
486
486
 
487
- | Code | Émis par | Déclencheur | Ce que voit le pair |
488
- | -------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- |
489
- | `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | requête vers une action non enregistrée | `method not found: <nom>` |
490
- | `-32603` | même méthode (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
491
- | `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
492
- | `-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 |
493
- | `-32602` | le pont API, via `RpcError` (`RealtimeController.ts#RealtimeController.actionParamsWrapper`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
494
- | `-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 |
495
-
496
- 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`).
487
+ | Code | Émis par | Déclencheur | Ce que voit le pair |
488
+ | -------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
489
+ | `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | requête vers une action non enregistrée | `method not found: <nom>` |
490
+ | `-32603` | même méthode (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
491
+ | `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
492
+ | `-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 |
493
+ | `-32602` | le pont API, via `RpcError` (`RealtimeController.ts#RealtimeController.actionParamsWrapper`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
494
+ | `-32000` | le pont API (`RealtimeController.ts#RealtimeController.invokeApiRequest`) | la route invoquée REFUSE : 401, 403, 404, 405, **422**, 429 | le message, `data.status` — `data.fields` sur un 422, `data.retryAfter` si l'action a posé `Retry-After` |
495
+
496
+ 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. Un refus passager (429, 503) dont l'action a posé `Retry-After` (RFC 9110 §10.2.3) porte l'attente dans `data.retryAfter`, en secondes — une date HTTP y est ramenée à l'écart : une socket ouverte n'a plus d'en-tête, et le même code de contrôleur (`this.response?.setHeader("Retry-After", n)` puis le refus) sert les deux transports. Seul cet en-tête traverse : rien d'autre posé par l'action ne fuit sur une trame. Tout autre `throw` reste opaque (`-32603`).
497
497
 
498
498
  Et un échec qui n'est **pas** une frame : l'expiration. `startCall()` ne reçoit rien dans le délai
499
499
  imparti, supprime l'entrée en attente et rejette localement avec `RPC timeout: <méthode>`
package/docs/securite.md CHANGED
@@ -449,9 +449,9 @@ mémoire : le token déjà résolu, et la cible de la frame.
449
449
  | Frame | Contrôle appliqué |
450
450
  | --- | --- |
451
451
  | `api.request {path}` | Re-match de zone HTTP : zone protégée + anonyme → refus |
452
- | `subscribe {channel}` | Plancher système, puis policy métier déclarée sur le canal |
453
- | notification `method` = canal inbound | Même politique que `subscribe` (on ne pousse pas sur un canal protégé) |
454
- | action `@RealtimeAction` | Authentifié **par défaut** ; rôle/scope si déclarés ; ouverte seulement si `{ authenticated: false }` est écrit |
452
+ | `subscribe {channel}` | Plancher système, puis policy de **LECTURE** déclarée sur le canal |
453
+ | notification `method` = canal inbound | Policy de lecture **ET** policy d'**ÉCRITURE** du même nom (on ne pousse pas sur un canal qu'on n'a pas le droit de lire) |
454
+ | action `@RealtimeAction` | Lecture **ET** écriture du nom. Sans politique : la lecture d'un canal homonyme si elle est déclarée, sinon authentifié **par défaut** ; ouverte seulement si `{ authenticated: false }` est écrit |
455
455
  | `ping`, `unsubscribe` | **passent** — pas de surface de données |
456
456
 
457
457
  Deux détails évitent des faux refus : `authorizeApiRequest()` (`frameAuthorizer.ts#authorizeApiRequest`) laisse
@@ -474,6 +474,22 @@ peut **resserrer**, jamais desserrer.
474
474
  Le journal d'audit est un **canal précis**, pas un namespace : sa règle est posée devant celle du
475
475
  territoire (premier match gagnant), et elle n'existe que si le hub réserve bien ce territoire.
476
476
 
477
+ **Les suffixes `:health` et `:stats` sont réservés dans TOUT espace de noms**, pas seulement sous
478
+ `nodefony:`. C'est la convention transverse des canaux de santé et de mesures : `net:health` est
479
+ donc refusé à quiconque ne porte pas `ROLE_NODEFONY_ADMIN`, même si l'application ne déclare rien
480
+ sur ce canal. Le hub, lui, ne réserve que `nodefony:` — d'où la surprise. Le refus le dit : sa
481
+ phrase nomme le suffixe, le rôle exigé et la règle qui le surcharge (`frameAuthorizer.ts#explainReservedSuffix`).
482
+ Pour ouvrir un tel canal à l'application, déclarer une **règle de préfixe** — placée avant les
483
+ défauts, elle gagne et la convention n'est plus consultée :
484
+
485
+ ```ts
486
+ use("@nodefony/security", {
487
+ realtimeChannels: [{ pattern: "net:", authenticated: true }],
488
+ });
489
+ ```
490
+
491
+ La règle ne descend pas sous ce qu'elle déclare : ici, `net:health` reste fermé à l'anonyme.
492
+
477
493
  Trois durcissements méritent d'être connus :
478
494
 
479
495
  - **Match insensible à la casse, sans allocation** — `startsWithCI()` (`frameAuthorizer.ts#buildSystemRules`).
@@ -494,9 +510,41 @@ jamais de canal d'audit non gardé.
494
510
 
495
511
  ### La policy métier — déclarer sur le canal
496
512
 
497
- `@RealtimeChannel(name, policy)` (`realtimeDecorators.ts#RealtimeChannel`) et `@RealtimeInbound(name, policy)`
498
- (`realtimeDecorators.ts#RealtimeInbound`) attachent un `IChannelPolicy` au **nom** du canal. Les trois axes sont
499
- cumulatifs (ET) ; un axe absent n'impose rien :
513
+ Deux politiques par **nom** de canal, chacune un `IChannelPolicy` :
514
+
515
+ - la **LECTURE** — `@RealtimeChannel(name, policy)` (`realtimeDecorators.ts#RealtimeChannel`) — exigée au
516
+ `subscribe` ;
517
+ - l'**ÉCRITURE** — `@RealtimeInbound(name, policy)` (`realtimeDecorators.ts#RealtimeInbound`),
518
+ `@RealtimeAction(name, policy)` — exigée **en plus** de la lecture sur toute trame entrante.
519
+
520
+ Ce qui n'est pas déclaré s'hérite, quel que soit l'ordre des méthodes : une lecture absente reprend
521
+ l'écriture (un canal gardé par `@RealtimeInbound` seul reste protégé en lecture), une action sans
522
+ politique reprend la lecture déclarée sous son nom, sinon le défaut fermé. Un canal que tout le monde
523
+ lit et que seuls certains écrivent s'écrit donc :
524
+
525
+ ```typescript
526
+ @RealtimeChannel("chat:typing", { authenticated: false }) // tout le monde voit « Untel écrit… »
527
+ typing(): () => void {
528
+ return () => {};
529
+ }
530
+ @RealtimeInbound("chat:typing", { authenticated: true }) // seuls les connectés écrivent
531
+ onTyping(): void {}
532
+ ```
533
+
534
+ Deux déclarations **contradictoires** d'une même sorte sur un nom — dans une classe, ou dans deux
535
+ classes — refusent le démarrage en nommant le canal : une seule s'appliquerait, l'autre
536
+ disparaîtrait. Une sous-classe qui redéclare la politique de son parent la surcharge. `allow` est
537
+ une règle de lecture : posé sur une écriture distincte de la lecture, il refuse le démarrage.
538
+
539
+ Avec des **motifs** (`room:*`), la lecture d'un nom appartient à la déclaration la plus spécifique
540
+ qui déclare quelque chose sur lui (le nom exact, sinon le motif au littéral le plus long) ; l'écriture
541
+ vient de la plus spécifique qui en déclare une, motifs compris. Une écriture de famille réservée
542
+ s'applique donc aussi à un nom exact qui n'a déclaré que sa lecture, et une commande déclarée par
543
+ `@RealtimeInbound("task:start")` sous une famille `task:*` à `allow` reste une commande. Le canal
544
+ personnel `user:<id>` garde sa règle « son canal seulement » tant qu'aucune déclaration ne vise
545
+ explicitement `user:` : un motif attrape-tout (`*`) ne la retire pas.
546
+
547
+ Les trois axes d'une politique sont cumulatifs (ET) ; un axe absent n'impose rien :
500
548
 
501
549
  | Axe | Sens | Évalué par |
502
550
  | --------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- |
@@ -504,14 +552,17 @@ cumulatifs (ET) ; un axe absent n'impose rien :
504
552
  | `roles` | un des rôles suffit, **hiérarchie comprise** | `Firewall.hasRole()` (`firewall.ts#Firewall.hasRole`) |
505
553
  | `scopes` | un des scopes suffit — axe API (JWT, clé API), une session BFF n'en porte pas | comparaison directe |
506
554
 
507
- Une policy **vide** n'est pas enregistrée : `definePolicy()` (`realtimeDecorators.ts#definePolicy`) ignore un
555
+ Une policy **vide** n'est pas enregistrée : `declarePolicy()` (`realtimeDecorators.ts#declarePolicy`) ignore un
508
556
  objet sans contrainte — le canal reste libre, le registre reste vide. Les déclarations sont inscrites
509
557
  au hub **au démarrage** (`channelPolicyRegistry.ts#registerControllerPolicies`, à `onKernelReady`) :
510
558
  un exemplaire qui n'a encore vu aucune poignée de main a déjà ses politiques — celui qui n'émet
511
559
  que des webhooks aussi. Un contrôleur échappé au démarrage s'inscrit à sa première poignée de main
512
560
  (`RealtimeHub.registerChannelPolicy()`, `RealtimeHub.ts#RealtimeHub.registerChannelPolicy`, idempotent) ;
513
- le décideur les relit par `RealtimeService.resolveChannelPolicy()`
514
- (`RealtimeService.ts#RealtimeService.resolveChannelPolicy`).
561
+ le décideur les relit en une résolution par `RealtimeService.resolveChannelPolicies()`
562
+ (`RealtimeService.ts#RealtimeService.resolveChannelPolicies`). Un refus d'écriture est audité sous
563
+ le motif `channel_write_policy`, distinct de `channel_policy` (lecture). Un verrou qui ne sait pas
564
+ juger l'écriture (il ne déclare pas `judgesWrite`) refuse le démarrage dès qu'une écriture distincte
565
+ est déclarée : versions de `@nodefony/realtime` et `@nodefony/security` à aligner.
515
566
 
516
567
  ### ⚠️ Une policy est attachée à un nom EXACT — les canaux dérivés n'héritent pas
517
568
 
@@ -808,7 +859,10 @@ Le refus doit être observable sans devenir un oracle. Nodefony tranche ainsi :
808
859
  | Canal sans aucun producteur | `realtime:denied { channel, reason: "unknown" }` |
809
860
 
810
861
  Le message est **générique** dans les trois cas : jamais « il te manque `ROLE_ADMIN` », jamais le nom
811
- de la zone. Sans `realtime:denied`, une notification refusée serait droppée en silence (elle n'a pas
862
+ de la zone. Hors production seulement, un champ `detail` ajoute la phrase qui aide le développeur
863
+ (`deniedDetail.ts#deniedDetail`) ; quand le verrou sait dire pourquoi il a refusé — un suffixe
864
+ `:health`/`:stats` réservé à la plateforme —, `detail` EST cette explication
865
+ (`RealtimeHub.ts#RealtimeHub.explainDenial`). Le journal du serveur la porte, lui, dans tous les modes. Sans `realtime:denied`, une notification refusée serait droppée en silence (elle n'a pas
812
866
  de canal de réponse) et le client se croirait abonné. **Aucun abonnement ne reste sans réponse** :
813
867
  un canal que personne ne produit — nom mal orthographié, module non chargé — rend `unknown`, qui
814
868
  n'ouvre aucun oracle (un canal gardé est tranché en amont, donc `forbidden` qu'il existe ou non).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/realtime",
3
- "version": "10.0.0-beta.6",
3
+ "version": "10.0.0-beta.7",
4
4
  "nodefony": {
5
5
  "configSchema": "./dist/nodefony/config/configSchema.js"
6
6
  },
@@ -44,27 +44,27 @@
44
44
  "esm"
45
45
  ],
46
46
  "peerDependencies": {
47
- "@nodefony/framework": "^10.0.0-beta.6",
48
- "@nodefony/http": "^10.0.0-beta.6",
49
- "nodefony": "^10.0.0-beta.6",
47
+ "@nodefony/framework": "^10.0.0-beta.7",
48
+ "@nodefony/http": "^10.0.0-beta.7",
49
+ "nodefony": "^10.0.0-beta.7",
50
50
  "reflect-metadata": "0.2.2",
51
51
  "zod": "^4.6.7"
52
52
  },
53
53
  "devDependencies": {
54
- "@nodefony/drizzle": "^10.0.0-beta.5",
55
- "@nodefony/framework": "^10.0.0-beta.6",
56
- "@nodefony/http": "^10.0.0-beta.6",
57
- "@nodefony/orm-core": "^10.0.0-beta.5",
58
- "@nodefony/security": "^10.0.0-beta.5",
54
+ "@nodefony/drizzle": "^10.0.0-beta.6",
55
+ "@nodefony/framework": "^10.0.0-beta.7",
56
+ "@nodefony/http": "^10.0.0-beta.7",
57
+ "@nodefony/orm-core": "^10.0.0-beta.6",
58
+ "@nodefony/security": "^10.0.0-beta.6",
59
59
  "@types/node": "26.6.6",
60
60
  "@vitest/coverage-v8": "5.1.0",
61
- "nodefony": "^10.0.0-beta.6",
61
+ "nodefony": "^10.0.0-beta.7",
62
62
  "reflect-metadata": "0.2.2",
63
63
  "vitest": "5.1.0"
64
64
  },
65
65
  "private": false,
66
66
  "engines": {
67
- "node": ">=24.14.0"
67
+ "node": "^24.16.0 || >=26.0.0"
68
68
  },
69
69
  "license": "Apache-2.0",
70
70
  "dependencies": {
@@ -83,7 +83,7 @@
83
83
  "url": "git+https://github.com/nodefony/nodefony-core.git",
84
84
  "directory": "src/packages/@nodefony/realtime"
85
85
  },
86
- "homepage": "https://nodefony.github.io/nodefony-core/",
86
+ "homepage": "https://nodefony.net/",
87
87
  "bugs": {
88
88
  "url": "https://github.com/nodefony/nodefony-core/issues"
89
89
  }