@nodefony/realtime 10.0.0-alpha.1

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 (81) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +207 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +218 -0
  6. package/dist/nodefony/config/config.js +63 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
  9. package/dist/nodefony/interfaces/IBackplane.js +1 -0
  10. package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
  11. package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
  12. package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
  13. package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
  14. package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
  15. package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
  16. package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
  17. package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
  18. package/dist/nodefony/interfaces/index.js +1 -0
  19. package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
  20. package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
  21. package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
  22. package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
  23. package/dist/nodefony/src/backplane/envelope.js +109 -0
  24. package/dist/nodefony/src/backplane/originId.js +35 -0
  25. package/dist/nodefony/src/backplane/publishQueue.js +96 -0
  26. package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
  27. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
  28. package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
  29. package/dist/nodefony/src/server/RealtimeController.js +601 -0
  30. package/dist/nodefony/src/server/RealtimeHub.js +926 -0
  31. package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
  32. package/dist/nodefony/src/server/deniedDetail.js +29 -0
  33. package/dist/nodefony/src/server/syslogUplink.js +105 -0
  34. package/dist/nodefony/src/server/welcomeEnv.js +25 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +240 -0
  36. package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
  37. package/dist/nodefony/testing/index.js +163 -0
  38. package/dist/types/index.d.ts +99 -0
  39. package/dist/types/nodefony/config/config.d.ts +43 -0
  40. package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
  41. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
  42. package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
  43. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
  44. package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
  45. package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
  46. package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
  48. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +18 -0
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
  53. package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
  54. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
  55. package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
  56. package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
  57. package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
  59. package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
  60. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
  61. package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
  65. package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
  66. package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
  67. package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
  68. package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
  69. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
  70. package/dist/types/nodefony/testing/index.d.ts +169 -0
  71. package/docs/actions.md +573 -0
  72. package/docs/architecture.md +889 -0
  73. package/docs/configuration.md +803 -0
  74. package/docs/cookbook-chat.md +1619 -0
  75. package/docs/couverture-bancs.md +139 -0
  76. package/docs/index.md +372 -0
  77. package/docs/observabilite.md +592 -0
  78. package/docs/protocole.md +518 -0
  79. package/docs/securite.md +845 -0
  80. package/docs/vocabulaire.md +845 -0
  81. package/package.json +83 -0
@@ -0,0 +1,163 @@
1
+ import { getRealtimeHub } from "../src/server/RealtimeHub.js";
2
+ //#region nodefony/testing/index.ts
3
+ /** `readyState` d'une connexion WebSocket ouverte (`ws` / DOM). */
4
+ const OPEN = 1;
5
+ /** Laisse tourner les microtâches et les timers immédiats. */
6
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
7
+ /**
8
+ * Attend qu'une condition devienne vraie, en bornant l'attente.
9
+ *
10
+ * Les chemins du controller mêlent synchrone (refus d'Origin) et asynchrone
11
+ * (authentification, action `async`) : attendre un nombre FIXE de tours rend un
12
+ * test fragile dans un sens et lent dans l'autre.
13
+ *
14
+ * @returns `true` si la condition s'est réalisée avant la borne.
15
+ */
16
+ async function waitFor(predicate, ticks) {
17
+ for (let i = 0; i < ticks; i++) {
18
+ if (predicate()) return true;
19
+ await tick();
20
+ }
21
+ return predicate();
22
+ }
23
+ /**
24
+ * Monte un controller temps réel sur une fausse connexion et rend de quoi le
25
+ * piloter.
26
+ *
27
+ * La fabrique reçoit le contexte à passer au constructeur : c'est la seule
28
+ * chose que le harnais ne peut pas deviner, puisque le nom du service et les
29
+ * dépendances injectées appartiennent au controller.
30
+ *
31
+ * @param create - construit le controller à partir du contexte fourni.
32
+ * @param options - écarts au décor par défaut (identité, Origin, verrou…).
33
+ * @returns le harnais, prêt pour `connect()`.
34
+ */
35
+ function createRealtimeHarness(create, options = {}) {
36
+ const hub = getRealtimeHub();
37
+ if (options.resetHub !== false) hub.clear();
38
+ if (options.identity !== void 0) {
39
+ const token = options.identity;
40
+ hub.useAuthenticator({ pattern: /.*/u }, {
41
+ name: "harness",
42
+ supports: () => true,
43
+ authenticate: () => Promise.resolve(token)
44
+ });
45
+ }
46
+ if (options.frameAuthorizer !== void 0) hub.setFrameAuthorizer(options.frameAuthorizer);
47
+ const url = options.url ?? "/realtime";
48
+ const received = [];
49
+ const closes = [];
50
+ const notices = [];
51
+ let onFinish = null;
52
+ let closed = false;
53
+ const connection = {
54
+ readyState: OPEN,
55
+ send: (data, cb) => {
56
+ received.push(JSON.parse(data));
57
+ cb?.();
58
+ },
59
+ close: (code, reason) => {
60
+ closes.push({
61
+ code,
62
+ reason
63
+ });
64
+ }
65
+ };
66
+ const controller = create({
67
+ connection,
68
+ once: (event, fn) => {
69
+ if (event === "onFinish") onFinish = fn;
70
+ },
71
+ request: {
72
+ headers: options.headers ?? {},
73
+ url
74
+ },
75
+ cookies: options.cookies ?? {},
76
+ url,
77
+ remoteAddress: "127.0.0.1",
78
+ origin: options.origin ?? ""
79
+ });
80
+ const bridge = controller;
81
+ let nextId = 1;
82
+ const feed = async (frame) => {
83
+ bridge.handleRealtime(frame === null ? null : JSON.stringify(frame));
84
+ await tick();
85
+ };
86
+ const rpc = (payload) => ({
87
+ jsonrpc: "2.0",
88
+ ...payload
89
+ });
90
+ return {
91
+ controller,
92
+ hub,
93
+ received,
94
+ closes,
95
+ notices,
96
+ async connect() {
97
+ await feed(null);
98
+ hub.onPlatformNotice((message) => {
99
+ notices.push(message);
100
+ });
101
+ if (!await waitFor(() => received.some((f) => f.method === "realtime:welcome"), 10)) {
102
+ const cause = closes.length > 0 ? `socket fermée (code ${String(closes[0]?.code)})` : "aucune frame émise";
103
+ throw new Error(`createRealtimeHarness: pas de realtime:welcome — ${cause}. Vérifier l'Origin (seam #4) et l'authenticator (seam #2).`);
104
+ }
105
+ return received.find((f) => f.method === "realtime:welcome");
106
+ },
107
+ async subscribe(channel) {
108
+ await feed(rpc({
109
+ method: "subscribe",
110
+ params: { channel }
111
+ }));
112
+ },
113
+ async unsubscribe(channel) {
114
+ await feed(rpc({
115
+ method: "unsubscribe",
116
+ params: { channel }
117
+ }));
118
+ },
119
+ async call(method, params) {
120
+ const id = nextId++;
121
+ await feed(rpc(params === void 0 ? {
122
+ id,
123
+ method
124
+ } : {
125
+ id,
126
+ method,
127
+ params
128
+ }));
129
+ const answered = await waitFor(() => received.some((f) => f.id === id), 20);
130
+ const response = received.find((f) => f.id === id);
131
+ if (!answered || response === void 0) throw new Error(`createRealtimeHarness: aucune réponse à l'action "${method}".`);
132
+ if (response.error !== void 0) throw Object.assign(/* @__PURE__ */ new Error(`${method} → ${response.error.message} (${String(response.error.code)})`), { rpc: response.error });
133
+ return response.result;
134
+ },
135
+ async notify(method, params) {
136
+ await feed(rpc(params === void 0 ? { method } : {
137
+ method,
138
+ params
139
+ }));
140
+ },
141
+ async send(frame) {
142
+ await feed(frame);
143
+ },
144
+ messages(channel) {
145
+ return received.filter((f) => f.method === channel && f.id === void 0).map((f) => f.params);
146
+ },
147
+ denials() {
148
+ return received.filter((f) => f.method === "realtime:denied").map((f) => f.params);
149
+ },
150
+ close() {
151
+ if (closed) return;
152
+ closed = true;
153
+ connection.readyState = 3;
154
+ onFinish?.();
155
+ },
156
+ dispose() {
157
+ this.close();
158
+ hub.clear();
159
+ }
160
+ };
161
+ }
162
+ //#endregion
163
+ export { createRealtimeHarness };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @nodefony/realtime — Couche realtime serveur Nodefony.
3
+ *
4
+ * Ce module porte le hub WebSocket (broker fan-out), le protocole JSON-RPC 2.0
5
+ * (peer isomorphe partagé avec le client core) et le backplane cluster. Trois
6
+ * drivers natifs : `loopback` (mono-process), `cluster` (IPC Node) et `redis`
7
+ * (multi-hôte) ; le registre `backplaneRegistry` est ouvert aux drivers écrits
8
+ * par l'application.
9
+ *
10
+ * Le wiring cluster (`onCluster` → branchement `ClusterBackplane` IPC +
11
+ * `ClusterProbeClient`) vit dans le hook du Module class ci-dessous. Le client
12
+ * isomorphe n'est PAS ici : il vit dans le cœur, subpath `nodefony/client`
13
+ * (décision figée — pas de package navigateur).
14
+ *
15
+ * Lire `docs/index.md` pour la vue d'ensemble vulgarisée.
16
+ *
17
+ * Voir aussi :
18
+ * - CLAUDE.md — décisions d'archi figées + 5 seams sécurité
19
+ * - MEMORY.md — internals IA
20
+ * - README.md — usage humain
21
+ * - docs/ — doc dev vulgarisée
22
+ */
23
+ import { Kernel, Module } from "nodefony";
24
+ import { type IRealtimeConfig, type IRealtimeConfigInput } from "./nodefony/config/defineModuleConfig.js";
25
+ import RealtimeService from "./nodefony/src/service/RealtimeService.js";
26
+ import RealtimeController from "./nodefony/src/server/RealtimeController.js";
27
+ import ServerRealtimeSocket, { serverSocket } from "./nodefony/src/server/ServerRealtimeSocket.js";
28
+ import RealtimeHub, { getRealtimeHub, SLOW_CONSUMER_BYTES, RESERVED_SYSTEM_PREFIXES, isReservedSystemChannel } from "./nodefony/src/server/RealtimeHub.js";
29
+ import LoopbackBackplane from "./nodefony/src/backplane/LoopbackBackplane.js";
30
+ import RedisBackplane, { createRedisServiceTransport, resolveRedisChannel, REDIS_RT_CHANNEL } from "./nodefony/src/backplane/RedisBackplane.js";
31
+ import { resolveBackplaneOriginId } from "./nodefony/src/backplane/originId.js";
32
+ import ClusterBackplane, { processIpcTransport } from "./nodefony/src/backplane/ClusterBackplane.js";
33
+ import { registerBackplaneDriver, getBackplaneDriver, listBackplaneDrivers } from "./nodefony/src/backplane/backplaneRegistry.js";
34
+ import ClusterProbeClient, { setClusterProbeClient, clusterProbeHealth, clusterProbeRequestEnrich, clusterProbeInstance, mergeClusterHealth, processProbeTransport } from "./nodefony/src/cluster/ClusterProbeClient.js";
35
+ import WsConnectionTransport from "./nodefony/src/transport/WsConnectionTransport.js";
36
+ import { createRealtimeAdminApi, buildRealtimeHealth, buildOwnHealth } from "./nodefony/src/server/RealtimeAdminApi.js";
37
+ declare module "nodefony" {
38
+ interface NodefonyModuleConfig {
39
+ "@nodefony/realtime": IRealtimeConfigInput;
40
+ }
41
+ }
42
+ declare class Realtime extends Module<IRealtimeConfig> {
43
+ #private;
44
+ /** Module optionnel : un échec de son boot ne tue jamais le process (résilience Ph.3). */
45
+ static critical: boolean;
46
+ constructor(kernel: Kernel);
47
+ /** JSON Schema de la config realtime → data plane admin (config riche Studio). */
48
+ configSchema(): unknown;
49
+ /**
50
+ * Validation Zod de la config racine merge au boot (convention figée 2026-05-28,
51
+ * cf [[feedback_config_validation_zod]]) — via le builder `defineRealtimeConfig`
52
+ * (source unique `nodefony/config/config.ts`). Plante propre avec messages clairs
53
+ * si la config (defaults + `module.options`) n'est pas conforme au schéma — évite
54
+ * tous les `undefined.x` silencieux en runtime.
55
+ *
56
+ * La config validée + gelée est exposée au container sous `realtimeConfig` pour
57
+ * que le `RealtimeService` (instancié à `onPreBoot` via `@services`) la consomme
58
+ * sans dupliquer la validation.
59
+ */
60
+ onKernelRegister(): Promise<this>;
61
+ /**
62
+ * Phase `onBoot` (avant le `mountAll` de framework dans `onReady`) — enregistre
63
+ * le producteur admin de la **socket Nodefony** (`/nodefony/realtime/api/*`)
64
+ * sur le broker `framework/AdminBroker`. Auto-observabilité du `RealtimeHub`.
65
+ *
66
+ * ⚠️ Ne PAS déplacer dans `onKernelReady` : framework fait `broker.mountAll()`
67
+ * dans SON `onKernelReady` ; les modules listés APRÈS framework dans `@modules()`
68
+ * (cas de @nodefony/realtime) verraient leur `register` rejeté avec
69
+ * « AdminBroker: register("realtime") après mountAll — routes figées ».
70
+ * `onKernelBoot` court donc avant et garantit l'ordre.
71
+ */
72
+ onKernelBoot(): Promise<this>;
73
+ }
74
+ export default Realtime;
75
+ 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 type { RealtimeConfig } from "./nodefony/config/config.js";
78
+ export { ANONYMOUS_REALTIME_TOKEN } from "./nodefony/src/server/AnonymousRealtimeToken.js";
79
+ export { defineRealtimeConfig, realtimeConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
80
+ export type { IRealtimeConfig, IRealtimeConfigInput, } from "./nodefony/config/defineModuleConfig.js";
81
+ export { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, getDeclaredBroadcastPrefixes, } from "./nodefony/decorators/realtimeDecorators.js";
82
+ export type { RealtimeChannelFactory } from "./nodefony/decorators/realtimeDecorators.js";
83
+ export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./nodefony/interfaces/IRealtimeController.js";
84
+ export type { IChannelPolicy } from "./nodefony/interfaces/IChannelPolicy.js";
85
+ export type { IClientLogsLimits } from "./nodefony/interfaces/IClientLogsLimits.js";
86
+ export { createSyslogUplinkHandler, BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, } from "./nodefony/src/server/syslogUplink.js";
87
+ export type { SyslogUplinkHandlerOptions, ClientSeverity, } from "./nodefony/src/server/syslogUplink.js";
88
+ export type { ChannelSink, ChannelFactory, } from "./nodefony/src/server/RealtimeHub.js";
89
+ export type { IBackplane, IBackplaneMessage, BackplaneHandler, IBackplaneInfo, } from "./nodefony/interfaces/IBackplane.js";
90
+ export type { IClusterBackplaneTransport, ClusterBackplaneEnvelope, } from "./nodefony/src/backplane/ClusterBackplane.js";
91
+ export type { IRedisBackplaneTransport, IRedisPublisher, IRedisSubscriber, } from "./nodefony/src/backplane/RedisBackplane.js";
92
+ export type { BackplaneFactory, IBackplaneFactoryContext, } from "./nodefony/src/backplane/backplaneRegistry.js";
93
+ export type { IClusterProbeTransport } from "./nodefony/src/cluster/ClusterProbeClient.js";
94
+ export type { RawWsConnection } from "./nodefony/src/transport/WsConnectionTransport.js";
95
+ export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./nodefony/interfaces/IRealtimeProbe.js";
96
+ export type { IRealtimeToken } from "./nodefony/interfaces/IRealtimeToken.js";
97
+ export type { IRealtimeHandshake } from "./nodefony/interfaces/IRealtimeHandshake.js";
98
+ export type { IRealtimeAuthenticator } from "./nodefony/interfaces/IRealtimeAuthenticator.js";
99
+ export type { IRealtimeAuthenticatorMatcher, ICompiledRealtimeMatcher, } from "./nodefony/interfaces/IRealtimeAuthenticatorMatcher.js";
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ export declare const realtimeConfigSchema: z.ZodObject<{
3
+ enabled: z.ZodDefault<z.ZodBoolean>;
4
+ backplane: z.ZodDefault<z.ZodObject<{
5
+ driver: z.ZodDefault<z.ZodString>;
6
+ namespace: z.ZodOptional<z.ZodString>;
7
+ secret: z.ZodOptional<z.ZodString>;
8
+ maxQueueBytes: z.ZodDefault<z.ZodNumber>;
9
+ }, z.core.$strict>>;
10
+ cluster: z.ZodDefault<z.ZodObject<{
11
+ probe: z.ZodDefault<z.ZodObject<{
12
+ enabled: z.ZodDefault<z.ZodBoolean>;
13
+ }, z.core.$strict>>;
14
+ }, z.core.$strict>>;
15
+ slowConsumer: z.ZodDefault<z.ZodObject<{
16
+ bytes: z.ZodDefault<z.ZodNumber>;
17
+ }, z.core.$strict>>;
18
+ limits: z.ZodDefault<z.ZodObject<{
19
+ maxChannelsPerConnection: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
20
+ }, z.core.$strict>>;
21
+ csrf: z.ZodDefault<z.ZodObject<{
22
+ checkOrigin: z.ZodDefault<z.ZodObject<{
23
+ enabled: z.ZodDefault<z.ZodBoolean>;
24
+ allowList: z.ZodDefault<z.ZodArray<z.ZodString>>;
25
+ allowMissingOrigin: z.ZodDefault<z.ZodBoolean>;
26
+ }, z.core.$strict>>;
27
+ }, z.core.$strict>>;
28
+ clientLogs: z.ZodDefault<z.ZodObject<{
29
+ enabled: z.ZodDefault<z.ZodBoolean>;
30
+ maxEntriesPerBatch: z.ZodDefault<z.ZodNumber>;
31
+ maxEntriesPerWindow: z.ZodDefault<z.ZodNumber>;
32
+ windowMs: z.ZodDefault<z.ZodNumber>;
33
+ maxStringLength: z.ZodDefault<z.ZodNumber>;
34
+ }, z.core.$strict>>;
35
+ }, z.core.$strict>;
36
+ /** Type de sortie (config normalisée + défauts appliqués). */
37
+ export type RealtimeConfig = z.infer<typeof realtimeConfigSchema>;
38
+ /**
39
+ * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
40
+ * valides par construction ; passés au `super(..., config)` du Module class.
41
+ */
42
+ declare const config: RealtimeConfig;
43
+ export default config;
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { realtimeConfigSchema, type RealtimeConfig } from "./config.js";
3
+ import type { IBackplane } from "../interfaces/IBackplane.js";
4
+ /**
5
+ * Builder type-safe de la configuration de `@nodefony/realtime`.
6
+ *
7
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
8
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
9
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
10
+ * env + freeze) et publie le JSON Schema Studio.
11
+ *
12
+ * Principes (alignés sur `defineSecurityConfig`) :
13
+ * - **Source unique** : `./config.ts` (Zod). Le builder VALIDE + GÈLE, ne dévie pas.
14
+ * - **Auto-documenté + introspectable** : chaque champ Zod porte `.describe()` →
15
+ * {@link realtimeConfigJsonSchema} produit un JSON Schema qu'un formulaire
16
+ * Studio (futur) consommera pour générer son UI d'édition.
17
+ * - **Backplane custom** : instance `IBackplane` userland (NATS, Pulsar…) passée
18
+ * via le 2ᵉ argument — **hors** schéma sérialisable (les classes n'ont rien à
19
+ * faire dans un JSON Schema Studio). L'instance, si fournie, prend précédence
20
+ * sur `driver` côté service ; ce dernier reste source d'introspection/UI.
21
+ *
22
+ * **Layering d'environnement** (convention-frère avec `REDIS_*` de
23
+ * `@nodefony/redis`) : `NF_REALTIME_DRIVER` surcharge le driver backplane
24
+ * APRÈS le parse (schéma pur, déterministe). Indispensable en déploiement
25
+ * (k8s/Docker : `NF_REALTIME_DRIVER=redis`) et fiable quel que soit le
26
+ * timing du merge `module-realtime` côté app (cf chantier ordering config).
27
+ *
28
+ * @param config - configuration brute (sections omises = défauts sûrs).
29
+ * @param options.backplane - instance `IBackplane` custom (override le driver à
30
+ * l'init du `RealtimeService`). Non sérialisable → exclue du JSON Schema.
31
+ * @returns config gelée prête pour `RealtimeService`.
32
+ * @throws ZodError si invalide.
33
+ */
34
+ export declare function defineRealtimeConfig(config?: IRealtimeConfigInput, options?: {
35
+ backplane?: IBackplane;
36
+ }): IRealtimeConfig;
37
+ /**
38
+ * JSON Schema introspectable de la config realtime — destiné au formulaire
39
+ * d'édition Studio (futur). N'inclut PAS `backplane.instance` (non sérialisable).
40
+ */
41
+ export declare function realtimeConfigJsonSchema(): unknown;
42
+ /** Entrée du builder (champs avec défaut optionnels). */
43
+ export type IRealtimeConfigInput = z.input<typeof realtimeConfigSchema>;
44
+ /** Config normalisée et gelée (sortie du builder, lue par `RealtimeService`). */
45
+ export type IRealtimeConfig = RealtimeConfig & {
46
+ backplane: RealtimeConfig["backplane"] & {
47
+ instance?: IBackplane;
48
+ };
49
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Décorateurs realtime de Nodefony — style déclaratif (NestJS-like) pour les
3
+ * endpoints WebSocket JSON-RPC 2.0 portés par {@link RealtimeController}.
4
+ *
5
+ * Les décorateurs **posent des métadonnées** sur le constructeur de la classe
6
+ * (via `reflect-metadata`, comme {@link controller}/{@link route} dans framework).
7
+ * La base {@link RealtimeController} les lit au handshake et fusionne avec les
8
+ * overrides classiques (`realtimeActions()` / `realtimeChannels()` /
9
+ * `realtimeInbound()` / `createRealtimeChannel()`) → coexistence sans casse.
10
+ *
11
+ * ── Pourquoi un nouveau fichier (pas dans framework/routerDecorators) ──
12
+ * Les décorateurs HTTP (`route`, `controller`) appartiennent au framework, qui
13
+ * ne dépend PAS de realtime (cycle interdit). Le realtime, qui PEUT dépendre du
14
+ * framework via peerDep, héberge ses propres décorateurs ici.
15
+ *
16
+ * ── Coût ──
17
+ * Lecture des métadonnées = 1× au handshake (cold path). Aucun coût par frame.
18
+ * Une classe sans décorateur (cas legacy) → Reflect.getMetadata renvoie
19
+ * `undefined` (pas d'allocation), fallback sur les overrides classiques.
20
+ */
21
+ import "reflect-metadata";
22
+ import type { RpcActionHandler } from "nodefony";
23
+ import type { RealtimePublish, RealtimeInboundHandler } from "../interfaces/IRealtimeController.js";
24
+ import type { IChannelPolicy } from "../interfaces/IChannelPolicy.js";
25
+ /**
26
+ * Politique appliquée à une action RPC dont l'auteur n'a rien déclaré :
27
+ * **exiger une connexion authentifiée**. Une action est une méthode que le pair
28
+ * APPELLE et qui AGIT — le défaut sûr est de la fermer, pas de l'ouvrir.
29
+ * Pour une action délibérément publique, l'écrire : `{ authenticated: false }`.
30
+ */
31
+ export declare const DEFAULT_ACTION_POLICY: IChannelPolicy;
32
+ /**
33
+ * Factory d'un provider de canal — appelée par la base au 1ᵉʳ `subscribe`.
34
+ *
35
+ * Signature alignée sur {@link RealtimeController.createRealtimeChannel} (la
36
+ * méthode override historique) pour qu'une migration d'une override `switch` vers
37
+ * des méthodes décorées reste un copier-coller : même `channel` (nom EXACT du
38
+ * sous-canal demandé, parfois suffixé `:<ms>` côté client mais ici on matche
39
+ * **strict**, cf {@link RealtimeChannel}) et même `publish`.
40
+ *
41
+ * Retour : `dispose` (idempotent) appelé au dernier `unsubscribe` ET au close
42
+ * de la connexion qui hébergeait le sink. `null` impossible pour une méthode
43
+ * décorée (un match exact réussi DOIT démarrer un provider) — c'est le contrat.
44
+ */
45
+ export type RealtimeChannelFactory = (channel: string, publish: RealtimePublish) => () => void;
46
+ /**
47
+ * Décorateur **méthode** — expose une **action RPC** request→response du
48
+ * controller realtime sous le nom `method` (ex. `"nodefony:kernel:ping"`, `"chat:send"`).
49
+ *
50
+ * La méthode décorée devient le handler appelé sur une requête entrante AVEC `id` ;
51
+ * sa valeur de retour est sérialisée comme `result`. Sync OU async (Promise) ;
52
+ * un throw est mappé sur `-32603 internal error` (Zero Trust — message générique
53
+ * au pair, détail loggé via `onError`).
54
+ *
55
+ * Le `this` est lié à l'instance de controller (`handler.bind(instance)` au
56
+ * handshake). La méthode peut donc lire `this.context`, `this.kernel`, etc.
57
+ *
58
+ * **Autorisation — FERMÉE PAR DÉFAUT.** Une action sans `policy` exige une
59
+ * connexion **authentifiée**. C'est délibéré : une action est une méthode que le
60
+ * pair appelle et qui AGIT (`nodefony:kernel:gc`, `nodefony:scaffold:run`, `orders:quote`), et le
61
+ * verrou de frame laisse passer tout ce qu'aucune politique ne couvre — sans ce
62
+ * défaut, toute action applicative serait publique sans que rien ne le dise.
63
+ * Pour une action délibérément ouverte, l'écrire : `{ authenticated: false }`.
64
+ *
65
+ * Si une politique est **déjà déclarée sous ce nom** (canal homonyme via
66
+ * `@RealtimeChannel`/`@RealtimeInbound`), elle est conservée : le défaut ne
67
+ * rétrograde jamais une politique existante, souvent plus stricte.
68
+ *
69
+ * ⚠️ Le NOM peut primer sur ta politique : le plancher système de
70
+ * `@nodefony/security` réserve les préfixes plateforme (`kernel:`, `syslog:`…)
71
+ * **et** tout nom contenant `:health` ou `:stats`. Une action ainsi nommée reste
72
+ * réservée même déclarée `{ authenticated: false }`.
73
+ *
74
+ * @param method - nom RPC exposé au pair.
75
+ * @param policy - politique d'autorisation ; par défaut `{ authenticated: true }`.
76
+ *
77
+ * @example
78
+ * class ChatController extends RealtimeController {
79
+ * @RealtimeAction("chat:ping") // authentifié requis
80
+ * ping(): { pong: true; ts: number } {
81
+ * return { pong: true, ts: Date.now() };
82
+ * }
83
+ *
84
+ * @RealtimeAction("admin:purge", { roles: ["ROLE_ADMIN"] })
85
+ * purge(): void {}
86
+ *
87
+ * @RealtimeAction("catalog:browse", { authenticated: false }) // ouvert, assumé
88
+ * browse(): string { return "ok"; }
89
+ * }
90
+ */
91
+ export declare function RealtimeAction(method: string, policy?: IChannelPolicy): MethodDecorator;
92
+ /**
93
+ * Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel`
94
+ * (match EXACT, sans préfixe `:<ms>` ni regex). La méthode décorée DOIT renvoyer
95
+ * un `dispose: () => void` (la base le mémorise et l'appelle au dernier
96
+ * désabonné, comme avec l'override classique).
97
+ *
98
+ * v1 — match EXACT uniquement. Les canaux à pattern (`/^nodefony:orm:rich@(\d+)/`) ou à
99
+ * suffixe de granularité (`nodefony:dashboard:1000`) restent dans l'override
100
+ * `createRealtimeChannel()` (la base donne PRIORITÉ aux décorateurs, puis tombe
101
+ * sur l'override en fallback — coexistence sans casse). Un mode `pattern: RegExp`
102
+ * arrivera plus tard sans rompre cette API.
103
+ *
104
+ * Le `this` est lié à l'instance ; le factory peut donc lire `this.kernel`, etc.
105
+ * Mais ATTENTION : un provider est **partagé** entre toutes les connexions au
106
+ * même canal (cf `RealtimeHub.subscribe`) — il survit à la connexion qui l'a
107
+ * créé. Capturer des **valeurs long-lived** (broker, syslog) dans la closure,
108
+ * jamais `this.context` (lié à la connexion créatrice qui peut fermer).
109
+ *
110
+ * @example
111
+ * class ChatController extends RealtimeController {
112
+ * @RealtimeChannel("chat:room42")
113
+ * room42(_channel: string, publish: RealtimePublish): () => void {
114
+ * const timer = setInterval(() => publish("chat:room42", { ts: Date.now() }), 1000);
115
+ * return () => clearInterval(timer);
116
+ * }
117
+ * }
118
+ */
119
+ export declare function RealtimeChannel(channel: string, policy?: IChannelPolicy): MethodDecorator;
120
+ /**
121
+ * Décorateur **méthode** — déclare un **canal FULL-DUPLEX entrant** (méthode WS
122
+ * inverse : le client pousse via une notification `method: <name>`, params libres,
123
+ * sans `id`). La méthode décorée reçoit `(params, reply)` :
124
+ * - `params` : NON FIABLE (entrée client) — valider AVANT d'agir.
125
+ * - `reply(payload)` : push serveur→client sur le MÊME canal, vers CETTE connexion.
126
+ *
127
+ * Équivalent décoratif de `realtimeInbound()` override. Sécurité : un canal n'est
128
+ * inbound QUE s'il est explicitement déclaré (défaut sûr — un client ne peut rien
129
+ * pousser tant qu'aucun handler n'est enregistré ici). Cf seam P13.8a
130
+ * (`beforeDispatch`) pour la gate sécurité par voter P6.
131
+ *
132
+ * @example
133
+ * class ChatController extends RealtimeController {
134
+ * @RealtimeInbound("chat:send")
135
+ * onChatSend(params: unknown, reply: (payload: unknown) => void): void {
136
+ * const text = (params as { text?: unknown })?.text;
137
+ * if (typeof text !== "string") return;
138
+ * reply({ ok: true, echoed: text });
139
+ * }
140
+ * }
141
+ */
142
+ export declare function RealtimeInbound(method: string, policy?: IChannelPolicy): MethodDecorator;
143
+ /** Lecture (lazy, au handshake) du registre des actions décorées. */
144
+ export declare function getRealtimeActions(instance: object): Record<string, RpcActionHandler> | null;
145
+ /** Lecture (lazy, au handshake) du registre des canaux décorés. */
146
+ export declare function getRealtimeChannels(instance: object): Record<string, RealtimeChannelFactory> | null;
147
+ /** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
148
+ export declare function getRealtimeInbound(instance: object): Record<string, RealtimeInboundHandler> | null;
149
+ /**
150
+ * Lecture (lazy, au handshake) des politiques d'autorisation déclarées
151
+ * (`@RealtimeChannel`/`@RealtimeInbound` avec opts). `null` si aucune → le
152
+ * controller n'enregistre rien au hub (0 alloc). Donnée pure (pas de `bind` :
153
+ * une policy n'est pas une méthode), renvoyée telle quelle pour publication au
154
+ * hub realtime, qui l'expose à `@nodefony/security`.
155
+ */
156
+ export declare function getRealtimeChannelPolicies(instance: object): Record<string, IChannelPolicy> | null;
157
+ /**
158
+ * Décorateur **classe** — déclare les préfixes de canaux **broadcast**
159
+ * (cross-process) servis par cet endpoint : leurs publications traversent le
160
+ * {@link IBackplane} au lieu de rester dans le processus.
161
+ *
162
+ * POURQUOI STATIQUE (trouvé sur un banc multi-pods) : la politique de forward
163
+ * est une propriété du code, pas de l'instant. Déclarée seulement au handshake
164
+ * — via l'override `realtimeBroadcastChannels()` — elle laissait un pod qui
165
+ * publie **sans abonné local** (job planifié, webhook, worker) ne rien
166
+ * propager : `publish` évaluait « pas broadcast », en silence. Ici la
167
+ * déclaration existe dès l'import de la classe ; le service l'applique au hub
168
+ * au boot, identiquement sur tous les pods.
169
+ *
170
+ * @example
171
+ * \@RealtimeBroadcast("chat:", "presence:")
172
+ * \@controller("/api/chat")
173
+ * class ChatController extends RealtimeController { … }
174
+ *
175
+ * @param prefixes - préfixes de canal (`"chat:"` couvre `chat:room1:1000`).
176
+ */
177
+ export declare function RealtimeBroadcast(...prefixes: string[]): ClassDecorator;
178
+ /** Préfixes broadcast déclarés par décorateur (introspection / tests). */
179
+ export declare function getDeclaredBroadcastPrefixes(): string[];
180
+ /**
181
+ * Applique au hub les préfixes broadcast déclarés statiquement. Appelé au boot
182
+ * par `RealtimeService` — avant tout trafic, donc la politique est en place que
183
+ * le pod ait ou non reçu une connexion. Idempotent
184
+ * ({@link RealtimeHub.markBroadcastChannel} l'est déjà).
185
+ *
186
+ * @param hub - hub à configurer (celui du processus).
187
+ */
188
+ export declare function applyDeclaredBroadcastPrefixes(hub: {
189
+ markBroadcastChannel(prefix: string): void;
190
+ }): void;