@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.
- package/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- 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;
|