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

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 (78) hide show
  1. package/README.md +79 -10
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +79 -12
  4. package/dist/nodefony/config/config.js +7 -0
  5. package/dist/nodefony/decorators/realtimeDecorators.js +125 -19
  6. package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
  7. package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
  8. package/dist/nodefony/src/backplane/RedisBackplane.js +143 -31
  9. package/dist/nodefony/src/backplane/controlOrder.js +57 -0
  10. package/dist/nodefony/src/backplane/envelope.js +113 -12
  11. package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
  12. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
  13. package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
  14. package/dist/nodefony/src/server/RealtimeConnection.js +110 -0
  15. package/dist/nodefony/src/server/RealtimeController.js +344 -85
  16. package/dist/nodefony/src/server/RealtimeHub.js +604 -61
  17. package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
  18. package/dist/nodefony/src/server/admissionBudget.js +176 -0
  19. package/dist/nodefony/src/server/channelPolicyRegistry.js +73 -0
  20. package/dist/nodefony/src/server/connectionAdmission.js +242 -0
  21. package/dist/nodefony/src/server/contractRoutes.js +85 -0
  22. package/dist/nodefony/src/server/deniedDetail.js +10 -3
  23. package/dist/nodefony/src/server/frameAdmission.js +168 -0
  24. package/dist/nodefony/src/server/subscribedAck.js +10 -1
  25. package/dist/nodefony/src/server/syslogUplink.js +3 -4
  26. package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
  27. package/dist/nodefony/src/service/RealtimeService.js +42 -1
  28. package/dist/nodefony/src/service/publicOrigin.js +40 -0
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
  30. package/dist/nodefony/testing/backplaneConformance.js +271 -0
  31. package/dist/nodefony/testing/index.js +21 -4
  32. package/dist/types/index.d.ts +34 -7
  33. package/dist/types/nodefony/config/config.d.ts +9 -0
  34. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +86 -6
  35. package/dist/types/nodefony/interfaces/IBackplane.d.ts +34 -50
  36. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
  37. package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
  38. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
  39. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
  40. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
  41. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  43. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
  44. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +79 -16
  45. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +52 -0
  46. package/dist/types/nodefony/src/backplane/envelope.d.ts +34 -0
  47. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
  48. package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
  49. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +17 -0
  50. package/dist/types/nodefony/src/server/RealtimeController.d.ts +93 -13
  51. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +321 -24
  52. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
  53. package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
  54. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +34 -0
  55. package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
  56. package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
  57. package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
  58. package/dist/types/nodefony/src/server/frameAdmission.d.ts +70 -0
  59. package/dist/types/nodefony/src/server/subscribedAck.d.ts +7 -0
  60. package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
  61. package/dist/types/nodefony/src/service/RealtimeService.d.ts +10 -2
  62. package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
  63. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
  64. package/dist/types/nodefony/testing/backplaneConformance.d.ts +66 -0
  65. package/dist/types/nodefony/testing/index.d.ts +17 -4
  66. package/docs/actions.md +442 -19
  67. package/docs/architecture.md +83 -8
  68. package/docs/cadence-et-contre-pression.md +87 -11
  69. package/docs/canaux-a-membres.md +267 -0
  70. package/docs/cas-d-usage.md +1702 -0
  71. package/docs/configuration.md +137 -17
  72. package/docs/cookbook-chat.md +333 -85
  73. package/docs/index.md +11 -1
  74. package/docs/observabilite.md +71 -14
  75. package/docs/protocole.md +133 -24
  76. package/docs/securite.md +294 -33
  77. package/docs/vocabulaire.md +13 -12
  78. package/package.json +14 -11
@@ -0,0 +1,271 @@
1
+ import { RealtimeHub } from "../src/server/RealtimeHub.js";
2
+ import { readControlOrder } from "../src/backplane/controlOrder.js";
3
+ import { PLATFORM_BUS_CHANNELS, PLATFORM_CHANNELS } from "nodefony";
4
+ import { randomBytes, randomUUID } from "node:crypto";
5
+ import assert from "node:assert/strict";
6
+ // nodefony/testing/backplaneConformance.ts
7
+ /**
8
+ * Suite de conformité d'un **driver de backplane** — la définition exécutable du
9
+ * contrat {@link IBackplane}, une seule pour tous les drivers (ADR-0022 §6,
10
+ * décision 4).
11
+ *
12
+ * Un driver écrit par un tiers (journal partitionné, bus de messages…) compile et
13
+ * démarre même mal réglé : un groupe de lecteurs partagé entre serveurs répartit
14
+ * les messages au lieu de les diffuser, une lecture depuis le début du journal
15
+ * rejoue des fins de partie sur des parties rouvertes. Rien ne lève, tout se perd
16
+ * en silence. Cette suite rend ces défauts visibles : elle relie plusieurs pairs du
17
+ * driver sur un même bus et vérifie ce que le hub tient pour acquis.
18
+ *
19
+ * ```ts
20
+ * import { describe, it } from "vitest";
21
+ * import { runBackplaneConformance } from "@nodefony/realtime/testing";
22
+ *
23
+ * runBackplaneConformance("mon-bus", (secret) => new MonBackplane({ url, secret }), { describe, it });
24
+ * ```
25
+ *
26
+ * Le lanceur de tests est PASSÉ, jamais importé : le paquet publié ne dépend
27
+ * d'aucun lanceur, et la suite tourne sous vitest, `node:test` ou `bun test`. Les
28
+ * assertions passent par `node:assert`, qu'ils rapportent tous.
29
+ *
30
+ * @packageDocumentation
31
+ */
32
+ /** Noms des cas — stables : un test négatif les désigne. */
33
+ const BACKPLANE_CONFORMANCE_CASES = {
34
+ fanout: "diffusion : chaque pair reçoit TOUT, l'émetteur ne reçoit pas son écho",
35
+ order: "ordre par émetteur : les publications d'un pair arrivent dans l'ordre",
36
+ noReplay: "pas de rejeu : un pair démarré après une publication ne la reçoit pas",
37
+ seal: "sceau : un message non scellé ou mal scellé est refusé",
38
+ control: "ordres de contrôle : le canal de contrôle traverse intact",
39
+ reserved: "espace réservé : refusé et compté à l'entrée du hub, la diffusion passe sans revenir",
40
+ replaceHandler: "onMessage remplace le handler précédent",
41
+ stop: "stop coupe la réception, et se rappelle sans erreur"
42
+ };
43
+ const POLL_MS = 5;
44
+ /** Charges de contrôle de fidélité : objet imbriqué, tableau, texte non ASCII. */
45
+ function sample(seq) {
46
+ return {
47
+ seq,
48
+ text: "é — 😀",
49
+ list: [
50
+ seq,
51
+ "a",
52
+ null,
53
+ { deep: true }
54
+ ]
55
+ };
56
+ }
57
+ function sleep(ms) {
58
+ return new Promise((resolve) => setTimeout(resolve, ms));
59
+ }
60
+ function seqOf(payload) {
61
+ return typeof payload === "object" && payload !== null && "seq" in payload ? payload.seq : void 0;
62
+ }
63
+ function isFrom(payload, from) {
64
+ return typeof payload === "object" && payload !== null && "from" in payload && payload.from === from;
65
+ }
66
+ /** Attend qu'une condition tienne, ou échoue en nommant ce qui manquait (lu à l'échec). */
67
+ async function until(done, timeoutMs, what) {
68
+ const deadline = Date.now() + timeoutMs;
69
+ while (!done()) {
70
+ if (Date.now() > deadline) assert.fail(`délai dépassé (${timeoutMs} ms) : ${what()}`);
71
+ await sleep(POLL_MS);
72
+ }
73
+ }
74
+ /**
75
+ * Boîte de réception d'un pair, filtrée sur les canaux du cas courant. Posée
76
+ * AVANT `start`, comme le hub le fait : un driver qui livrerait dès l'abonnement
77
+ * (un rejeu, par exemple) est vu.
78
+ */
79
+ var Inbox = class {
80
+ messages = [];
81
+ #prefix;
82
+ constructor(peer, prefix) {
83
+ this.#prefix = prefix;
84
+ peer.onMessage((msg) => {
85
+ if (msg.channel.startsWith(this.#prefix)) this.messages.push(msg);
86
+ });
87
+ }
88
+ payloads() {
89
+ return this.messages.map((m) => m.payload);
90
+ }
91
+ };
92
+ /**
93
+ * Déclare la suite de conformité d'un driver de backplane auprès du lanceur de
94
+ * tests de l'appelant.
95
+ *
96
+ * Vérifie, sur plusieurs pairs reliés au même bus : la diffusion à TOUS les pairs
97
+ * sans écho, l'ordre par émetteur, l'absence de rejeu au démarrage, le sceau
98
+ * (message non scellé ou mal scellé refusé), le transport des ordres de contrôle,
99
+ * le refus de l'espace réservé à l'entrée du hub, le remplacement du handler et
100
+ * l'arrêt.
101
+ *
102
+ * @param name - nom du driver, affiché dans le rapport.
103
+ * @param makePeer - fabrique d'un pair relié au bus commun.
104
+ * @param options - le lanceur (`describe`, `it`) et les réglages.
105
+ */
106
+ function runBackplaneConformance(name, makePeer, options) {
107
+ const sealed = options.sealed ?? true;
108
+ const quietMs = options.quietMs ?? 100;
109
+ const timeoutMs = options.timeoutMs ?? 2e3;
110
+ const secret = sealed ? randomBytes(32).toString("hex") : null;
111
+ const C = BACKPLANE_CONFORMANCE_CASES;
112
+ /**
113
+ * Cadre d'un cas : pairs ouverts puis tous arrêtés, et un préfixe de canal
114
+ * unique — un bus partagé entre cas ne mélange jamais leurs messages.
115
+ */
116
+ const scenario = (body) => {
117
+ return async () => {
118
+ const peers = [];
119
+ const prefix = `conformance:${randomUUID()}:`;
120
+ const open = async (peerSecret = secret) => {
121
+ const peer = await makePeer(peerSecret);
122
+ peers.push(peer);
123
+ const inbox = new Inbox(peer, prefix);
124
+ await peer.start();
125
+ return {
126
+ peer,
127
+ inbox
128
+ };
129
+ };
130
+ try {
131
+ await body(open, prefix);
132
+ } finally {
133
+ await Promise.all(peers.map(async (p) => {
134
+ await p.stop();
135
+ }));
136
+ }
137
+ };
138
+ };
139
+ options.describe(`conformité du backplane « ${name} »`, () => {
140
+ options.it(C.fanout, scenario(async (open, prefix) => {
141
+ const a = await open();
142
+ const b = await open();
143
+ const c = await open();
144
+ const sent = Array.from({ length: 10 }, (_, i) => sample(i));
145
+ for (const p of sent) a.peer.publish(`${prefix}fanout`, p);
146
+ await until(() => b.inbox.messages.length >= sent.length && c.inbox.messages.length >= sent.length, timeoutMs, () => `B et C attendaient ${sent.length} messages, reçus ${b.inbox.messages.length} et ${c.inbox.messages.length} — le bus répartit-il les messages entre pairs ?`);
147
+ b.peer.publish(`${prefix}fanout`, { seq: -1 });
148
+ await until(() => a.inbox.messages.some((m) => m.originId === b.peer.originId), timeoutMs, () => "A n'a pas reçu la sentinelle de B");
149
+ await sleep(quietMs);
150
+ assert.deepEqual(b.inbox.payloads(), sent, "B : charges reçues fidèlement");
151
+ assert.deepEqual(c.inbox.messages.filter((m) => m.originId === a.peer.originId).map((m) => m.payload), sent, "C : charges reçues fidèlement");
152
+ for (const m of b.inbox.messages) assert.equal(m.originId, a.peer.originId);
153
+ assert.ok(a.inbox.messages.every((m) => m.originId === b.peer.originId), "l'émetteur a reçu son propre écho");
154
+ }));
155
+ options.it(C.order, scenario(async (open, prefix) => {
156
+ const a = await open();
157
+ const b = await open();
158
+ const count = 100;
159
+ for (let i = 0; i < count; i += 1) a.peer.publish(`${prefix}order`, { seq: i });
160
+ await until(() => b.inbox.messages.length >= count, timeoutMs, () => `B attendait ${count} messages, reçus ${b.inbox.messages.length}`);
161
+ assert.deepEqual(b.inbox.payloads().map(seqOf), Array.from({ length: count }, (_, i) => i), "ordre d'émission perdu — la clé de partition doit être l'émetteur, jamais le canal");
162
+ }));
163
+ options.it(C.noReplay, scenario(async (open, prefix) => {
164
+ const a = await open();
165
+ const witness = await open();
166
+ a.peer.publish(`${prefix}replay`, { seq: 0 });
167
+ await until(() => witness.inbox.messages.length === 1, timeoutMs, () => "le témoin n'a pas reçu la première publication");
168
+ const late = await open();
169
+ a.peer.publish(`${prefix}replay`, { seq: 1 });
170
+ await until(() => late.inbox.messages.some((m) => seqOf(m.payload) === 1), timeoutMs, () => "le pair démarré en retard n'a pas reçu la publication suivante");
171
+ await sleep(quietMs);
172
+ assert.deepEqual(late.inbox.payloads().map(seqOf), [1], "rejeu au démarrage — lire à partir du dernier message, jamais depuis le début");
173
+ }));
174
+ options.it(C.seal, scenario(async (open, prefix) => {
175
+ if (!sealed) {
176
+ const { peer } = await open(null);
177
+ assert.notEqual(peer.describe().crossPod, true, "un bus qui quitte la machine (crossPod) doit sceller : retirer `sealed: false`");
178
+ return;
179
+ }
180
+ const receiver = await open();
181
+ const bare = await open(null);
182
+ const forged = await open(randomBytes(32).toString("hex"));
183
+ const genuine = await open();
184
+ bare.peer.publish(`${prefix}seal`, { from: "non scellé" });
185
+ forged.peer.publish(`${prefix}seal`, { from: "autre secret" });
186
+ genuine.peer.publish(`${prefix}seal`, { from: "scellé" });
187
+ await until(() => receiver.inbox.messages.some((m) => isFrom(m.payload, "scellé")), timeoutMs, () => "le message scellé du bon secret n'est pas arrivé");
188
+ await sleep(quietMs);
189
+ assert.deepEqual(receiver.inbox.payloads(), [{ from: "scellé" }], "un message non scellé ou mal scellé a été accepté");
190
+ }));
191
+ options.it(C.control, scenario(async (open, prefix) => {
192
+ const a = await open();
193
+ const b = await open();
194
+ const got = [];
195
+ b.peer.onMessage((msg) => {
196
+ if (msg.channel !== PLATFORM_BUS_CHANNELS.control) return;
197
+ const order = readControlOrder(msg.payload);
198
+ if (order?.channel.startsWith(prefix) === true) got.push(order);
199
+ });
200
+ const orders = [{
201
+ op: "close",
202
+ channel: `${prefix}room`
203
+ }, {
204
+ op: "evictUser",
205
+ channel: `${prefix}chat`,
206
+ user: "u-42",
207
+ generation: 3
208
+ }];
209
+ for (const o of orders) a.peer.publish(PLATFORM_BUS_CHANNELS.control, o);
210
+ await until(() => got.length >= orders.length, timeoutMs, () => `ordres de contrôle reçus : ${got.length} sur ${orders.length}`);
211
+ assert.deepEqual(got, orders);
212
+ }));
213
+ options.it(C.reserved, scenario(async (open, prefix) => {
214
+ const a = await open();
215
+ const b = await open();
216
+ const hubA = new RealtimeHub();
217
+ const hubB = new RealtimeHub();
218
+ try {
219
+ hubA.setBackplane(a.peer);
220
+ hubB.setBackplane(b.peer);
221
+ hubA.markBroadcastChannel(prefix);
222
+ hubB.markBroadcastChannel(prefix);
223
+ const noop = () => () => {};
224
+ const channel = `${prefix}room`;
225
+ const gotA = [];
226
+ const gotB = [];
227
+ const audit = [];
228
+ hubA.subscribe(channel, (p) => gotA.push(p), noop);
229
+ hubB.subscribe(channel, (p) => gotB.push(p), noop);
230
+ hubB.subscribe(PLATFORM_CHANNELS.audit, (p) => audit.push(p), noop);
231
+ a.peer.publish(PLATFORM_CHANNELS.audit, { forged: "faux évènement d'audit" });
232
+ hubA.publish(channel, { ok: 1 });
233
+ await until(() => gotB.length > 0, timeoutMs, () => "la diffusion déclarée n'a pas atteint le hub B");
234
+ await sleep(quietMs);
235
+ assert.deepEqual(audit, [], "un canal réservé a atteint les abonnés locaux");
236
+ assert.equal(hubB.probe().ingressRejectedTotal, 1, "rejet non compté");
237
+ assert.deepEqual(gotB, [{ ok: 1 }]);
238
+ assert.deepEqual(gotA, [{ ok: 1 }], "message renvoyé sur le bus (boucle)");
239
+ } finally {
240
+ hubA.clear();
241
+ hubB.clear();
242
+ }
243
+ }));
244
+ options.it(C.replaceHandler, scenario(async (open, prefix) => {
245
+ const a = await open();
246
+ const b = await open();
247
+ const second = [];
248
+ b.peer.onMessage((m) => {
249
+ if (m.channel.startsWith(prefix)) second.push(m.payload);
250
+ });
251
+ a.peer.publish(`${prefix}handler`, { seq: 0 });
252
+ await until(() => second.length > 0, timeoutMs, () => "le second handler n'a rien reçu");
253
+ await sleep(quietMs);
254
+ assert.equal(b.inbox.messages.length, 0, "l'ancien handler reçoit encore");
255
+ assert.deepEqual(second, [{ seq: 0 }]);
256
+ }));
257
+ options.it(C.stop, scenario(async (open, prefix) => {
258
+ const a = await open();
259
+ const b = await open();
260
+ const witness = await open();
261
+ await b.peer.stop();
262
+ await b.peer.stop();
263
+ const after = new Inbox(b.peer, prefix);
264
+ a.peer.publish(`${prefix}stop`, { seq: 0 });
265
+ await until(() => witness.inbox.messages.length > 0, timeoutMs, () => "le témoin n'a pas reçu la publication");
266
+ await sleep(quietMs);
267
+ assert.equal(after.messages.length, 0, "un pair arrêté reçoit encore");
268
+ }));
269
+ });
270
+ }
271
+ export { BACKPLANE_CONFORMANCE_CASES, runBackplaneConformance };
@@ -1,5 +1,7 @@
1
1
  import { isRecord } from "../src/guards.js";
2
+ import { createAdmissionResolver, registerControllerPolicies } from "../src/server/channelPolicyRegistry.js";
2
3
  import { getRealtimeHub } from "../src/server/RealtimeHub.js";
4
+ import { BACKPLANE_CONFORMANCE_CASES, runBackplaneConformance } from "./backplaneConformance.js";
3
5
  import { RequestContext } from "nodefony";
4
6
  // nodefony/testing/index.ts
5
7
  /**
@@ -61,7 +63,8 @@ async function waitFor(predicate, ticks) {
61
63
  const DENIED_REASONS = {
62
64
  forbidden: true,
63
65
  limit: true,
64
- unknown: true
66
+ unknown: true,
67
+ unavailable: true
65
68
  };
66
69
  function isDeniedReason(reason) {
67
70
  return Object.hasOwn(DENIED_REASONS, reason);
@@ -114,6 +117,8 @@ function readDenied(params) {
114
117
  };
115
118
  throw new Error(`createRealtimeHarness: realtime:denied malformé — ${JSON.stringify(params)}`);
116
119
  }
120
+ /** Compteur des vhosts de harnais — un par harnais à identité, jamais réutilisé. */
121
+ let harnessCount = 0;
117
122
  /**
118
123
  * Monte un controller temps réel sur une fausse connexion et rend de quoi le
119
124
  * piloter.
@@ -129,9 +134,18 @@ function readDenied(params) {
129
134
  function createRealtimeHarness(create, options = {}) {
130
135
  const hub = getRealtimeHub();
131
136
  if (options.resetHub !== false) hub.clear();
137
+ let headers = options.headers ?? {};
132
138
  if (options.identity !== void 0) {
133
139
  const token = options.identity;
134
- hub.useAuthenticator({ pattern: /.*/u }, {
140
+ const host = headers["host"] ?? `harness-${++harnessCount}.invalid`;
141
+ headers = {
142
+ ...headers,
143
+ host
144
+ };
145
+ hub.useAuthenticator({
146
+ pattern: /.*/u,
147
+ host
148
+ }, {
135
149
  name: "harness",
136
150
  supports: () => true,
137
151
  authenticate: () => Promise.resolve(token)
@@ -159,12 +173,13 @@ function createRealtimeHarness(create, options = {}) {
159
173
  };
160
174
  const controller = create({
161
175
  type: "websocket",
176
+ outboundTracer: () => null,
162
177
  connection,
163
178
  once: (event, fn) => {
164
179
  if (event === "onFinish") onFinish = fn;
165
180
  },
166
181
  request: {
167
- headers: options.headers ?? {},
182
+ headers,
168
183
  url
169
184
  },
170
185
  cookies: options.cookies ?? {},
@@ -172,6 +187,8 @@ function createRealtimeHarness(create, options = {}) {
172
187
  remoteAddress: "127.0.0.1",
173
188
  origin: options.origin ?? ""
174
189
  });
190
+ const admissions = options.admissions;
191
+ if (admissions !== void 0) registerControllerPolicies(hub, controller.constructor, createAdmissionResolver((_key, admissionClass) => admissions.find((admission) => admission instanceof admissionClass), () => {}));
175
192
  const bridge = controller;
176
193
  let nextId = 1;
177
194
  const identity = options.identity;
@@ -275,4 +292,4 @@ function createRealtimeHarness(create, options = {}) {
275
292
  }
276
293
  };
277
294
  }
278
- export { createRealtimeHarness };
295
+ export { BACKPLANE_CONFORMANCE_CASES, createRealtimeHarness, runBackplaneConformance };
@@ -27,7 +27,7 @@ import RealtimeController from "./nodefony/src/server/RealtimeController.js";
27
27
  import ServerRealtimeSocket, { serverSocket } from "./nodefony/src/server/ServerRealtimeSocket.js";
28
28
  import RealtimeHub, { getRealtimeHub, SLOW_CONSUMER_BYTES, RESERVED_SYSTEM_PREFIXES, isReservedSystemChannel } from "./nodefony/src/server/RealtimeHub.js";
29
29
  import LoopbackBackplane from "./nodefony/src/backplane/LoopbackBackplane.js";
30
- import RedisBackplane, { createRedisServiceTransport, resolveRedisChannel, REDIS_RT_CHANNEL } from "./nodefony/src/backplane/RedisBackplane.js";
30
+ import RedisBackplane, { createRedisServiceTransport, resolveRedisChannel, resolveRedisBytesChannel, REDIS_RT_CHANNEL } from "./nodefony/src/backplane/RedisBackplane.js";
31
31
  import { resolveBackplaneOriginId } from "./nodefony/src/backplane/originId.js";
32
32
  import ClusterBackplane, { processIpcTransport } from "./nodefony/src/backplane/ClusterBackplane.js";
33
33
  import { registerBackplaneDriver, getBackplaneDriver, listBackplaneDrivers } from "./nodefony/src/backplane/backplaneRegistry.js";
@@ -70,26 +70,53 @@ declare class Realtime extends Module<IRealtimeConfig> {
70
70
  * `onKernelBoot` court donc avant et garantit l'ordre.
71
71
  */
72
72
  onKernelBoot(): Promise<this>;
73
+ /**
74
+ * Phase `onReady` — inscrit au hub les politiques de canal de TOUS les contrôleurs temps réel connus, et résout leurs classes d'admission, avant que le moindre serveur n'écoute.
75
+ *
76
+ * Pas à `onKernelBoot` : chaque module enregistre ses contrôleurs dans SON
77
+ * `onBoot` (`@controllers`), et ceux d'un module listé après celui-ci n'y
78
+ * seraient pas encore. `onReady` passe après tous les `onBoot` et avant
79
+ * `initServers` : aucune poignée de main ne précède l'inscription, et un
80
+ * exemplaire qui n'émet que des webhooks a ses politiques comme les autres.
81
+ *
82
+ * @throws BootConfigurationError Politique illisible, `allow` sur une
83
+ * action, classe d'admission sans singleton, sans `allow` ou dont une
84
+ * dépendance manque — la classe et le canal sont nommés.
85
+ */
86
+ onKernelReady(): Promise<this>;
87
+ /**
88
+ * Les routes montées par le routeur du framework, vues par le contrôle des
89
+ * routes de contrat : chemin, méthodes, schémas d'entrée.
90
+ *
91
+ * @returns la liste, vide si le routeur du framework est absent.
92
+ */
93
+ private mountedRoutes;
73
94
  }
74
95
  export default Realtime;
75
96
  export { Realtime };
76
- export { RealtimeController, ServerRealtimeSocket, serverSocket, RealtimeHub, getRealtimeHub, SLOW_CONSUMER_BYTES, RESERVED_SYSTEM_PREFIXES, isReservedSystemChannel, LoopbackBackplane, ClusterBackplane, RedisBackplane, createRedisServiceTransport, resolveRedisChannel, resolveBackplaneOriginId, REDIS_RT_CHANNEL, registerBackplaneDriver, getBackplaneDriver, listBackplaneDrivers, processIpcTransport, ClusterProbeClient, setClusterProbeClient, processProbeTransport, clusterProbeHealth, clusterProbeRequestEnrich, clusterProbeInstance, mergeClusterHealth, WsConnectionTransport, createRealtimeAdminApi, buildRealtimeHealth, buildOwnHealth, RealtimeService, };
77
- export { ANONYMOUS_REALTIME_TOKEN } from "./nodefony/src/server/AnonymousRealtimeToken.js";
97
+ export { RealtimeController, ServerRealtimeSocket, serverSocket, RealtimeHub, getRealtimeHub, SLOW_CONSUMER_BYTES, RESERVED_SYSTEM_PREFIXES, isReservedSystemChannel, LoopbackBackplane, ClusterBackplane, RedisBackplane, createRedisServiceTransport, resolveRedisChannel, resolveRedisBytesChannel, resolveBackplaneOriginId, REDIS_RT_CHANNEL, registerBackplaneDriver, getBackplaneDriver, listBackplaneDrivers, processIpcTransport, ClusterProbeClient, setClusterProbeClient, processProbeTransport, clusterProbeHealth, clusterProbeRequestEnrich, clusterProbeInstance, mergeClusterHealth, WsConnectionTransport, createRealtimeAdminApi, buildRealtimeHealth, buildOwnHealth, RealtimeService, };
98
+ export { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_SUBSCRIBER_IDENTITY, } from "./nodefony/src/server/AnonymousRealtimeToken.js";
78
99
  export { defineRealtimeConfig, realtimeConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
79
100
  export type { IRealtimeConfig, IRealtimeConfigInput, } from "./nodefony/config/defineModuleConfig.js";
80
- export { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, getDeclaredBroadcastPrefixes, } from "./nodefony/decorators/realtimeDecorators.js";
101
+ export { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, getDeclaredBroadcastPrefixes, } from "./nodefony/decorators/realtimeDecorators.js";
81
102
  export type { RealtimeChannelFactory } from "./nodefony/decorators/realtimeDecorators.js";
82
- export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./nodefony/interfaces/IRealtimeController.js";
103
+ export { ContractRealtimeController, ContractRealtimeControllerBase, ContractRealtimeControllerStrict, } from "./nodefony/src/server/ContractRealtimeController.js";
104
+ export type { ContractRealtimeMethodHandlers, ContractPolicyNames, RealtimeContractOf, } from "./nodefony/src/server/ContractRealtimeController.js";
105
+ export type { IContractRealtimeHub } from "./nodefony/src/server/RealtimeHub.js";
106
+ export type { IRealtimeController, RealtimePublish, ContractRealtimePublish, RealtimeInboundHandler, TypedRealtimeInboundHandler, RealtimeInboundHandlers, RealtimeActionHandler, } from "./nodefony/interfaces/IRealtimeController.js";
83
107
  export type { IChannelPolicy } from "./nodefony/interfaces/IChannelPolicy.js";
84
108
  export type { IRealtimeConnectionState } from "./nodefony/interfaces/IRealtimeConnectionState.js";
109
+ export type { IRealtimeConnection } from "./nodefony/interfaces/IRealtimeConnection.js";
85
110
  export type { IClientLogsLimits } from "./nodefony/interfaces/IClientLogsLimits.js";
86
111
  export { createSyslogUplinkHandler, BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, } from "./nodefony/src/server/syslogUplink.js";
87
112
  export type { SyslogUplinkHandlerOptions, ClientSeverity, } from "./nodefony/src/server/syslogUplink.js";
88
- export type { ChannelSink, ChannelFactory, } from "./nodefony/src/server/RealtimeHub.js";
113
+ export type { ChannelSink, ChannelFactory, ChannelFrameBuilder, } from "./nodefony/src/server/RealtimeHub.js";
114
+ export { ChannelFrame } from "./nodefony/src/server/RealtimeHub.js";
89
115
  export type { IBackplane, IBackplaneMessage, BackplaneHandler, IBackplaneInfo, } from "./nodefony/interfaces/IBackplane.js";
90
116
  export type { IClusterBackplaneTransport, ClusterBackplaneEnvelope, } from "./nodefony/src/backplane/ClusterBackplane.js";
91
- export type { IRedisBackplaneTransport, IRedisPublisher, IRedisSubscriber, } from "./nodefony/src/backplane/RedisBackplane.js";
117
+ export type { IRedisBackplaneTransport, IRedisPublisher, IRedisSubscriber, IRedisBytesSubscriber, } from "./nodefony/src/backplane/RedisBackplane.js";
92
118
  export type { BackplaneFactory, IBackplaneFactoryContext, } from "./nodefony/src/backplane/backplaneRegistry.js";
119
+ export { sealBackplaneEnvelope, openBackplaneEnvelope, } from "./nodefony/src/backplane/envelope.js";
93
120
  export type { IClusterProbeTransport } from "./nodefony/src/cluster/ClusterProbeClient.js";
94
121
  export type { RawWsConnection } from "./nodefony/src/transport/WsConnectionTransport.js";
95
122
  export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./nodefony/interfaces/IRealtimeProbe.js";
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  export declare const realtimeConfigSchema: z.ZodObject<{
3
3
  enabled: z.ZodDefault<z.ZodBoolean>;
4
+ codec: z.ZodDefault<z.ZodEnum<{
5
+ cbor: "cbor";
6
+ json: "json";
7
+ }>>;
4
8
  backplane: z.ZodDefault<z.ZodObject<{
5
9
  driver: z.ZodDefault<z.ZodString>;
6
10
  namespace: z.ZodOptional<z.ZodString>;
@@ -18,6 +22,11 @@ export declare const realtimeConfigSchema: z.ZodObject<{
18
22
  limits: z.ZodDefault<z.ZodObject<{
19
23
  maxChannelsPerConnection: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
20
24
  }, z.core.$strict>>;
25
+ authenticateTimeoutMs: z.ZodDefault<z.ZodNumber>;
26
+ allowTimeoutMs: z.ZodDefault<z.ZodNumber>;
27
+ maxPendingAllowsPerConnection: z.ZodDefault<z.ZodNumber>;
28
+ maxPendingAllowsTotal: z.ZodDefault<z.ZodNumber>;
29
+ publicOrigin: z.ZodOptional<z.ZodString>;
21
30
  csrf: z.ZodDefault<z.ZodObject<{
22
31
  checkOrigin: z.ZodDefault<z.ZodObject<{
23
32
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -19,9 +19,28 @@
19
19
  * `undefined` (pas d'allocation), fallback sur les overrides classiques.
20
20
  */
21
21
  import "reflect-metadata";
22
- import type { RpcActionHandler } from "nodefony";
22
+ import type { ChannelAdmissionClass, ChannelAllow, RpcActionHandler } from "nodefony";
23
23
  import type { RealtimePublish, RealtimeInboundHandler } from "../interfaces/IRealtimeController.js";
24
24
  import type { IChannelPolicy } from "../interfaces/IChannelPolicy.js";
25
+ import type { ContractPolicyNames, RealtimeContractOf } from "../src/server/ContractRealtimeController.js";
26
+ /**
27
+ * `allow` désigne-t-il une classe d'admission (`@injectable`) plutôt qu'une flèche ?
28
+ *
29
+ * Tranché par la métadonnée `@injectable`, jamais par le texte de la fonction
30
+ * (`class …`), qu'un minifieur réécrit (ADR-0021 §S10 bis).
31
+ *
32
+ * @param allow - valeur du membre `allow` d'une politique déjà validée.
33
+ * @returns `true` pour une classe à résoudre par l'injecteur.
34
+ */
35
+ export declare function isAdmissionClass(allow: ChannelAllow): allow is ChannelAdmissionClass;
36
+ /**
37
+ * Défaut d'une politique de canal, ou `null` si elle est recevable — en clair, pour que le refus au démarrage dise QUOI corriger.
38
+ *
39
+ * @param value - valeur lue sous la métadonnée des politiques (`unknown` :
40
+ * `Reflect.defineMetadata` est public).
41
+ * @returns la raison du refus, ou `null`.
42
+ */
43
+ export declare function channelPolicyDefect(value: unknown): string | null;
25
44
  /**
26
45
  * Politique appliquée à une action RPC dont l'auteur n'a rien déclaré :
27
46
  * **exiger une connexion authentifiée**. Une action est une méthode que le pair
@@ -89,6 +108,50 @@ export type RealtimeChannelFactory = (channel: string, publish: RealtimePublish)
89
108
  * }
90
109
  */
91
110
  export declare function RealtimeAction(method: string, policy?: IChannelPolicy): MethodDecorator;
111
+ /**
112
+ * Noms d'une table de politiques absents du contrat de la classe décorée —
113
+ * `never` quand tous y sont, ou que la classe n'a pas de contrat.
114
+ */
115
+ type NamesNotInContract<P, T> = [RealtimeContractOf<T>] extends [never] ? never : Exclude<keyof P, ContractPolicyNames<RealtimeContractOf<T>>>;
116
+ /**
117
+ * Contrainte posée sur la classe décorée : aucune si les noms sont tous dans le
118
+ * contrat ; sinon un membre `notInContract` exigé, que l'erreur affiche avec les
119
+ * noms fautifs.
120
+ */
121
+ type PolicyNamesCheck<P, T> = [NamesNotInContract<P, T>] extends [never] ? unknown : {
122
+ readonly notInContract: NamesNotInContract<P, T>;
123
+ };
124
+ /**
125
+ * Décorateur de **classe** — pose les politiques d'accès d'actions ou de canaux
126
+ * par leur NOM, sans déclarer de handler.
127
+ *
128
+ * C'est la voie des actions servies par la table de `realtimeActions()` : cette
129
+ * table ne sait pas porter de politique, et un `@RealtimeAction` ajouté pour la
130
+ * seule politique ferait fabriquer, à chaque connexion, un handler aussitôt
131
+ * écrasé par la table (et contournerait l'injection de ses paramètres décorés).
132
+ * Ici, rien par connexion : les politiques sont écrites UNE fois, à la
133
+ * définition de la classe, dans le même registre que celles des décorateurs de
134
+ * méthode. Une action de la table sans politique garde le défaut FERMÉ
135
+ * (connexion authentifiée).
136
+ *
137
+ * Un nom qui porte `*` est un motif (famille de canaux), comme partout ailleurs.
138
+ *
139
+ * **Sur un contrôleur de contrat** (`extends ContractRealtimeController<C>`), le
140
+ * décorateur lit le contrat sur la classe qu'il décore : chaque nom doit être
141
+ * une méthode ou un canal de `C`, sinon la classe ne compile pas, et l'erreur
142
+ * nomme le fautif (`notInContract`). Le contrat ne se répète pas. Sur un
143
+ * contrôleur sans contrat, tout nom est accepté. Vérification à la compilation
144
+ * seulement : coût d'exécution nul.
145
+ *
146
+ * @param policies - nom d'action ou de canal → politique.
147
+ *
148
+ * @example
149
+ * @RealtimePolicy({ "orders:quote": { roles: ["ROLE_SALES"] } })
150
+ * class OrdersRt extends ContractRealtimeController<OrdersContract> { … }
151
+ */
152
+ export declare function RealtimePolicy<P extends {
153
+ readonly [name: string]: IChannelPolicy | undefined;
154
+ }>(policies: P): <T extends abstract new (...args: never[]) => object>(target: T & PolicyNamesCheck<P, T>) => void;
92
155
  /**
93
156
  * Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel`
94
157
  * (match EXACT, sans préfixe `:<ms>` ni regex). La méthode décorée DOIT renvoyer
@@ -164,11 +227,27 @@ export declare function getRealtimeChannels(instance: object): Record<string, Re
164
227
  /** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
165
228
  export declare function getRealtimeInbound(instance: object): Record<string, RealtimeInboundHandler> | null;
166
229
  /**
167
- * Lecture (lazy, au handshake) des politiques d'autorisation déclarées
168
- * (`@RealtimeChannel`/`@RealtimeInbound` avec opts). `null` si aucune → le
169
- * controller n'enregistre rien au hub (0 alloc). Donnée pure (pas de `bind` :
170
- * une policy n'est pas une méthode), renvoyée telle quelle pour publication au
171
- * hub realtime, qui l'expose à `@nodefony/security`.
230
+ * La classe déclare-t-elle une ACTION (`@RealtimeAction`) sous ce nom ? Sert à refuser un `allow` posé sur une action, que rien n'appliquerait.
231
+ *
232
+ * @param ctor - constructeur du contrôleur.
233
+ * @param name - nom d'action ou de canal.
234
+ */
235
+ export declare function declaresAction(ctor: object, name: string): boolean;
236
+ /**
237
+ * Politiques d'autorisation déclarées sur une CLASSE de contrôleur (`@RealtimeChannel`/`@RealtimeInbound`/`@RealtimePolicy` avec politique), validées — lues au démarrage du noyau, jamais par trame.
238
+ *
239
+ * @param ctor - constructeur du contrôleur.
240
+ * @returns nom de canal ou de méthode → politique, ou `null` si la classe n'en
241
+ * déclare aucune (0 allocation).
242
+ * @throws BootConfigurationError Une politique est illisible : la classe, le
243
+ * canal et le défaut sont nommés.
244
+ */
245
+ export declare function getChannelPoliciesOf(ctor: object): Record<string, IChannelPolicy> | null;
246
+ /**
247
+ * Politiques déclarées sur la classe d'une instance de contrôleur — même lecture que {@link getChannelPoliciesOf}.
248
+ *
249
+ * @param instance - contrôleur instancié.
250
+ * @returns nom → politique, ou `null` si aucune.
172
251
  */
173
252
  export declare function getRealtimeChannelPolicies(instance: object): Record<string, IChannelPolicy> | null;
174
253
  /**
@@ -205,3 +284,4 @@ export declare function getDeclaredBroadcastPrefixes(): string[];
205
284
  export declare function applyDeclaredBroadcastPrefixes(hub: {
206
285
  markBroadcastChannel(prefix: string): void;
207
286
  }): void;
287
+ export {};