@nodefony/realtime 10.0.0-beta.4 → 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/nodefony/src/server/RealtimeController.js +4 -4
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +2 -2
- package/dist/types/nodefony/testing/index.d.ts +5 -3
- package/docs/actions.md +23 -1
- package/docs/cookbook-chat.md +19 -0
- package/docs/securite.md +10 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -163,20 +163,39 @@ authenticator aurait résolue, et le verrou de frame que `@nodefony/security` po
|
|
|
163
163
|
Sans verrou, une `policy` déclarée n'est appliquée par personne — c'est vrai en test comme
|
|
164
164
|
en production, `@nodefony/realtime` ne connaissant pas `@nodefony/security`.
|
|
165
165
|
|
|
166
|
+
Les deux sont fournis par `@nodefony/security/testing` — le verrou est celui du pare-feu, pas
|
|
167
|
+
une copie :
|
|
168
|
+
|
|
166
169
|
```ts
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
import {
|
|
171
|
+
createTestFrameAuthorizer,
|
|
172
|
+
createTestIdentity,
|
|
173
|
+
} from "@nodefony/security/testing";
|
|
174
|
+
|
|
175
|
+
// `chat:room` exige une connexion authentifiée. Sans `identity`, le harnais
|
|
176
|
+
// pose le jeton anonyme, comme le repli du runtime : le canal est refusé.
|
|
177
|
+
const anonyme = createRealtimeHarness((ctx) => new ChatController(ctx), {
|
|
178
|
+
frameAuthorizer: createTestFrameAuthorizer(getRealtimeHub()),
|
|
174
179
|
});
|
|
175
|
-
await
|
|
176
|
-
await
|
|
177
|
-
expect(
|
|
180
|
+
await anonyme.connect();
|
|
181
|
+
await anonyme.subscribe("chat:room");
|
|
182
|
+
expect(anonyme.denials().map((d) => d.channel)).toEqual(["chat:room"]);
|
|
183
|
+
anonyme.dispose();
|
|
184
|
+
|
|
185
|
+
// Avec une identité, le même canal est accordé.
|
|
186
|
+
const alice = createRealtimeHarness((ctx) => new ChatController(ctx), {
|
|
187
|
+
identity: createTestIdentity({ identifier: "alice", roles: ["ROLE_USER"] }),
|
|
188
|
+
frameAuthorizer: createTestFrameAuthorizer(getRealtimeHub()),
|
|
189
|
+
});
|
|
190
|
+
await alice.connect();
|
|
191
|
+
await alice.subscribe("chat:room");
|
|
192
|
+
expect(alice.denials()).toHaveLength(0);
|
|
193
|
+
alice.dispose();
|
|
178
194
|
```
|
|
179
195
|
|
|
196
|
+
Les deux cas vont ensemble : le refus seul passerait aussi avec un canal fermé à tous, l'accord
|
|
197
|
+
seul avec un canal ouvert à tous.
|
|
198
|
+
|
|
180
199
|
| Membre | Rôle |
|
|
181
200
|
| ------------------------------------------- | --------------------------------------------------------------- |
|
|
182
201
|
| `connect()` | Handshake ; rend le `realtime:welcome`, jette s'il n'arrive pas |
|
|
@@ -193,7 +193,7 @@ var RealtimeController = class extends Controller {
|
|
|
193
193
|
return {};
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
|
-
* Opt-in du **pont API** (
|
|
196
|
+
* Opt-in du **pont API** (une action, deux transports) : `true` → la
|
|
197
197
|
* connexion expose la méthode RPC `api.request {path}` qui re-route le path
|
|
198
198
|
* porté par le message vers l'action controller correspondante — la MÊME
|
|
199
199
|
* action que le GET REST (`socket.request("/nodefony/kernel/api/modules")`
|
|
@@ -602,7 +602,7 @@ var RealtimeController = class extends Controller {
|
|
|
602
602
|
};
|
|
603
603
|
}
|
|
604
604
|
/**
|
|
605
|
-
* Handler de la méthode RPC `api.request {path}` — le **pont
|
|
605
|
+
* Handler de la méthode RPC `api.request {path}` — le **pont socket**
|
|
606
606
|
* (POC Ph.3) : résout le path porté par le message via le Router
|
|
607
607
|
* (`cleanPathOverride` — l'URL de la connexion n'est jamais mutée) puis
|
|
608
608
|
* exécute l'action SANS la rendre (`executeAction`) : la valeur nue est
|
|
@@ -713,11 +713,11 @@ var RealtimeController = class extends Controller {
|
|
|
713
713
|
}
|
|
714
714
|
});
|
|
715
715
|
frame?.finish(200);
|
|
716
|
-
return frame ? new RpcEnvelope(result, { requestId: frame.requestId }) : result;
|
|
716
|
+
return frame?.profiled ? new RpcEnvelope(result, { requestId: frame.requestId }) : result;
|
|
717
717
|
} catch (e) {
|
|
718
718
|
const status = httpStatusOfFrameError(e);
|
|
719
719
|
frame?.finish(status ?? 500, e);
|
|
720
|
-
throw toFrameRpcError(e, status, frame?.requestId);
|
|
720
|
+
throw toFrameRpcError(e, status, frame?.profiled ? frame.requestId : void 0);
|
|
721
721
|
} finally {
|
|
722
722
|
if (store !== null) RequestContext.release(store);
|
|
723
723
|
ctx.collectFrame(frame);
|
|
@@ -91,7 +91,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
91
91
|
*/
|
|
92
92
|
protected realtimeInbound(): Record<string, RealtimeInboundHandler>;
|
|
93
93
|
/**
|
|
94
|
-
* Opt-in du **pont API** (
|
|
94
|
+
* Opt-in du **pont API** (une action, deux transports) : `true` → la
|
|
95
95
|
* connexion expose la méthode RPC `api.request {path}` qui re-route le path
|
|
96
96
|
* porté par le message vers l'action controller correspondante — la MÊME
|
|
97
97
|
* action que le GET REST (`socket.request("/nodefony/kernel/api/modules")`
|
|
@@ -245,7 +245,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
245
245
|
*/
|
|
246
246
|
private actionParamsWrapper;
|
|
247
247
|
/**
|
|
248
|
-
* Handler de la méthode RPC `api.request {path}` — le **pont
|
|
248
|
+
* Handler de la méthode RPC `api.request {path}` — le **pont socket**
|
|
249
249
|
* (POC Ph.3) : résout le path porté par le message via le Router
|
|
250
250
|
* (`cleanPathOverride` — l'URL de la connexion n'est jamais mutée) puis
|
|
251
251
|
* exécute l'action SANS la rendre (`executeAction`) : la valeur nue est
|
|
@@ -88,9 +88,11 @@ export interface IRealtimeHarnessOptions {
|
|
|
88
88
|
/**
|
|
89
89
|
* Verrou de frame (seam #1). Neutre par construction : `@nodefony/realtime`
|
|
90
90
|
* ne connaît pas `@nodefony/security`. Une application qui charge la sécurité
|
|
91
|
-
* passe ici
|
|
92
|
-
*
|
|
93
|
-
*
|
|
91
|
+
* passe ici `createTestFrameAuthorizer(getRealtimeHub())` de
|
|
92
|
+
* `@nodefony/security/testing` — le verrou du pare-feu, jamais une copie —
|
|
93
|
+
* pour éprouver une politique de canal ou une action gardée par rôle. Sans
|
|
94
|
+
* lui, une `policy` déclarée n'est appliquée par personne, exactement comme
|
|
95
|
+
* au runtime.
|
|
94
96
|
*/
|
|
95
97
|
readonly frameAuthorizer?: FrameAuthorizer;
|
|
96
98
|
/**
|
package/docs/actions.md
CHANGED
|
@@ -638,7 +638,28 @@ describe("ChatController — actions", () => {
|
|
|
638
638
|
C'est l'attribut `user` du jeton que `@CurrentUser()` rend. Pour éprouver un **refus** (`@IsGranted`,
|
|
639
639
|
`policy` de canal), il faut en plus le verrou de frame de `@nodefony/security`, passé en
|
|
640
640
|
`frameAuthorizer` : `@nodefony/realtime` ne connaît pas la sécurité, et sans ce verrou une règle
|
|
641
|
-
déclarée n'est appliquée par personne — en test comme en production.
|
|
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
|
|
642
663
|
harnais et l'exemple de refus sont dans le `README.md` du paquet, section « Tester un controller ».
|
|
643
664
|
`nodefony create controller --kind realtime` génère ce test, identité comprise, quand l'application
|
|
644
665
|
porte la sécurité.
|
|
@@ -656,3 +677,4 @@ Couverture : `npm run coverage` dans `@nodefony/realtime`.
|
|
|
656
677
|
- La garde qui décide, côté HTTP comme WebSocket → [firewall](../../security/docs/firewall.md)
|
|
657
678
|
- Les signatures exactes ne sont pas recopiées ici : elles vivent dans le graphe symbolique
|
|
658
679
|
`.ai/symbols.json`, régénéré depuis les TSDoc du code.
|
|
680
|
+
````
|
package/docs/cookbook-chat.md
CHANGED
|
@@ -35,6 +35,25 @@ coverageModule: realtime
|
|
|
35
35
|
|
|
36
36
|
📍 [Documentation](../../../../../docs/index.md) › [@nodefony/realtime](index.md) › **Cookbook — un chat**
|
|
37
37
|
|
|
38
|
+
## ⚡ Le geste — deux commandes avant d'écrire une ligne
|
|
39
|
+
|
|
40
|
+
Une ressource REST et un canal temps réel ne s'écrivent pas à la main : les générateurs de
|
|
41
|
+
l'application les posent, câblés et testés. Depuis la racine de l'application :
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx nodefony create entity Message body:text room:string:index --route /api/messages
|
|
45
|
+
npx nodefony create controller room --kind realtime
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Commande | Ce qu'elle pose |
|
|
49
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `create entity` | la table (`nodefony/entity/Message.ts`), ses schémas de validation, un service CRUD, `MessageController` — REST sur `/api/messages`, et ses lectures répondent aussi par la socket —, un test unitaire et un test e2e, le tout inscrit dans `index.ts` — puis, si l'application versionne ses migrations (toute application complète), la migration de la table, écrite et appliquée à la base de développement ; sinon la table naît au prochain démarrage |
|
|
51
|
+
| `create controller --kind realtime` | `RoomController` : la socket `/api/room/realtime`, le canal `room:events`, ses actions, et un test qui la pilote sans serveur ni navigateur |
|
|
52
|
+
|
|
53
|
+
La suite de ce cookbook construit le même salon À LA MAIN, dans un seul contrôleur : c'est ce qui
|
|
54
|
+
permet de comprendre ce que les générateurs écrivent, et de le retoucher. Pour avancer, partir des
|
|
55
|
+
générateurs ; pour comprendre, lire la suite.
|
|
56
|
+
|
|
38
57
|
## 🎯 Ce qu'on construit
|
|
39
58
|
|
|
40
59
|
**En une phrase** : un salon de discussion où plusieurs personnes échangent des messages et des
|
package/docs/securite.md
CHANGED
|
@@ -644,11 +644,20 @@ La source de vérité côté security est `buildSessionRevalidator()`
|
|
|
644
644
|
et vérifie qu'elle est toujours vivante **et toujours celle du même utilisateur** — ce second point
|
|
645
645
|
attrape le changement de compte sur un navigateur partagé.
|
|
646
646
|
|
|
647
|
+
**Un changement de rôles coupe sur-le-champ, sans attendre le tick.** Quand une mise à jour du compte
|
|
648
|
+
touche ses rôles, le hub ferme toutes les sockets de cette identité avec le code **4005**
|
|
649
|
+
(`realtime/closeCodes.ts#IDENTITY_REFRESH_CLOSE_CODE`, `RealtimeHub.ts#RealtimeHub.#revocation`). À la
|
|
650
|
+
différence de 4001 (identité morte, définitif), 4005 est **reconnectable** : le client se reconnecte
|
|
651
|
+
seul et reçoit ses nouveaux rôles dans l'accueil. Les flux SSE du même compte suivent la même règle.
|
|
652
|
+
Aucune lecture de base par tick ni par frame : le coût n'existe qu'à la mise à jour qui touche
|
|
653
|
+
`roles`.
|
|
654
|
+
|
|
647
655
|
> [!CAUTION]
|
|
648
656
|
> **Trois limites à connaître.**
|
|
649
657
|
>
|
|
650
658
|
> 1. **Fenêtre de 30 s.** Entre le logout et le tick suivant, les flux continuent. Pour une coupure
|
|
651
|
-
> immédiate, fermer la socket explicitement côté serveur.
|
|
659
|
+
> immédiate, fermer la socket explicitement côté serveur. (Un changement de **rôles**, lui, coupe
|
|
660
|
+
> immédiatement — ci-dessus.)
|
|
652
661
|
> 2. **Best-effort si la session est illisible au handshake.** Si l'id ou le store ne sont pas
|
|
653
662
|
> accessibles, `buildSessionRevalidator()` renvoie `null` ; `UserRealtimeToken.isValid()`
|
|
654
663
|
> (`UserRealtimeToken.ts#UserRealtimeToken.isValid`) répond alors toujours `true`. La connexion **est** inscrite au
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.5",
|
|
4
4
|
"nodefony": {
|
|
5
5
|
"configSchema": "./dist/nodefony/config/configSchema.js"
|
|
6
6
|
},
|
|
@@ -44,18 +44,18 @@
|
|
|
44
44
|
"esm"
|
|
45
45
|
],
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
48
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
49
|
-
"nodefony": "^10.0.0-beta.
|
|
47
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
48
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
49
|
+
"nodefony": "^10.0.0-beta.5",
|
|
50
50
|
"reflect-metadata": "0.2.2",
|
|
51
51
|
"zod": "^4.6.7"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
55
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
54
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
55
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
56
56
|
"@types/node": "26.6.5",
|
|
57
57
|
"@vitest/coverage-v8": "5.0.5",
|
|
58
|
-
"nodefony": "^10.0.0-beta.
|
|
58
|
+
"nodefony": "^10.0.0-beta.5",
|
|
59
59
|
"reflect-metadata": "0.2.2",
|
|
60
60
|
"vitest": "5.0.5"
|
|
61
61
|
},
|