@aegis-fluxion/core 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # @aegis-fluxion/core
2
2
 
3
- Low-level E2E-encrypted WebSocket primitives for the `aegis-fluxion` ecosystem.
3
+ Low-level encrypted WebSocket primitives for the `aegis-fluxion` ecosystem.
4
4
 
5
- If you prefer a single user-facing package, use [`aegis-fluxion`](../aegis-fluxion/README.md).
6
-
7
- Version: **0.7.1**
5
+ Version: **0.7.2**
8
6
 
9
7
  ---
10
8
 
@@ -12,16 +10,11 @@ Version: **0.7.1**
12
10
 
13
11
  - Ephemeral ECDH handshake (`prime256v1`)
14
12
  - AES-256-GCM encrypted envelopes
15
- - Encrypted ACK request/response (`Promise` and callback)
16
- - Secure room routing (`join`, `leave`, `leaveAll`, `to(room).emit`)
17
- - Heartbeat and zombie socket cleanup
18
- - Auto-reconnect with fresh re-handshake
19
- - **Binary payload support**: `Buffer`, `Uint8Array`, `Blob`
20
- - **Server middleware pipeline** via `SecureServer.use(...)`
13
+ - ACK request/response (Promise + callback styles)
14
+ - Secure room routing (`join`, `leave`, `leaveAll`, `to(room).emit(...)`)
21
15
  - Middleware phases: `connection`, `incoming`, `outgoing`
22
- - Per-socket middleware metadata available as `SecureServerClient.metadata`
23
- - **Rate limiting & DDoS shield** with per-connection/per-IP controls
24
- - Optional MCP bridge package: `@aegis-fluxion/mcp-adapter`
16
+ - Rate limiting and DDoS controls per connection and IP
17
+ - **Horizontal scaling hooks** via pluggable `SecureServerAdapter`
25
18
 
26
19
  ---
27
20
 
@@ -33,84 +26,126 @@ npm install @aegis-fluxion/core ws
33
26
 
34
27
  ---
35
28
 
36
- ## Middleware in 0.6.0+
29
+ ## SecureServer adapter API (horizontal scaling)
37
30
 
38
- `SecureServer` now supports phase-based middleware for auth, policy enforcement, and payload
39
- normalization.
31
+ ### Core types
40
32
 
41
33
  ```ts
42
- import { SecureClient, SecureServer } from "@aegis-fluxion/core";
34
+ export interface SecureServerAdapterMessage {
35
+ version: 1;
36
+ originServerId: string;
37
+ scope: "broadcast" | "room";
38
+ event: string;
39
+ data: unknown;
40
+ emittedAt: number;
41
+ room?: string;
42
+ }
43
+
44
+ export interface SecureServerAdapter {
45
+ attach(server: SecureServer): void | Promise<void>;
46
+ publish(message: SecureServerAdapterMessage): void | Promise<void>;
47
+ detach?(server: SecureServer): void | Promise<void>;
48
+ }
49
+ ```
43
50
 
44
- const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
51
+ ### SecureServer hooks
45
52
 
46
- server.use(async (context, next) => {
47
- if (context.phase === "connection") {
48
- const rawApiKey = context.request.headers["x-api-key"];
49
- const apiKey = Array.isArray(rawApiKey) ? rawApiKey[0] : rawApiKey;
53
+ - constructor option: `new SecureServer({ ..., adapter })`
54
+ - runtime binding: `await server.setAdapter(adapter)`
55
+ - inbound relay: `await server.handleAdapterMessage(message)`
56
+ - instance identity: `server.serverId`
57
+
58
+ ### Message normalization helper
59
+
60
+ ```ts
61
+ import { normalizeSecureServerAdapterMessage } from "@aegis-fluxion/core";
62
+ ```
63
+
64
+ Use it in adapters before delivering inbound Pub/Sub payloads to `SecureServer`.
65
+
66
+ ---
67
+
68
+ ## Adapter integration example
50
69
 
51
- if (apiKey !== "dev-secret") {
52
- throw new Error("Unauthorized");
70
+ ```ts
71
+ import {
72
+ SecureServer,
73
+ type SecureServerAdapter,
74
+ type SecureServerAdapterMessage
75
+ } from "@aegis-fluxion/core";
76
+
77
+ class InMemoryAdapter implements SecureServerAdapter {
78
+ private static readonly peers = new Set<InMemoryAdapter>();
79
+ private server: SecureServer | null = null;
80
+
81
+ async attach(server: SecureServer): Promise<void> {
82
+ this.server = server;
83
+ InMemoryAdapter.peers.add(this);
84
+ }
85
+
86
+ async publish(message: SecureServerAdapterMessage): Promise<void> {
87
+ for (const peer of InMemoryAdapter.peers) {
88
+ if (peer === this || !peer.server) {
89
+ continue;
90
+ }
91
+
92
+ await peer.server.handleAdapterMessage(message);
93
+ }
94
+ }
95
+
96
+ async detach(server: SecureServer): Promise<void> {
97
+ if (this.server !== server) {
98
+ return;
53
99
  }
54
100
 
55
- context.metadata.set("role", "editor");
101
+ this.server = null;
102
+ InMemoryAdapter.peers.delete(this);
56
103
  }
104
+ }
57
105
 
58
- await next();
106
+ const server = new SecureServer({
107
+ host: "127.0.0.1",
108
+ port: 8080,
109
+ adapter: new InMemoryAdapter()
59
110
  });
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Middleware and ACK example
116
+
117
+ ```ts
118
+ import { SecureClient, SecureServer } from "@aegis-fluxion/core";
119
+
120
+ const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
60
121
 
61
122
  server.use(async (context, next) => {
62
- if (
63
- context.phase === "incoming" &&
64
- context.event === "post:create" &&
65
- typeof context.data === "object" &&
66
- context.data !== null
67
- ) {
68
- const payload = context.data as { title?: string };
69
- context.data = { title: String(payload.title ?? "").trim() };
123
+ if (context.phase === "connection") {
124
+ context.metadata.set("auth.role", "operator");
70
125
  }
71
126
 
72
127
  await next();
73
-
74
- if (context.phase === "outgoing" && context.event === "post:create") {
75
- context.data = {
76
- ...(context.data as Record<string, unknown>),
77
- middleware: true
78
- };
79
- }
80
128
  });
81
129
 
82
- server.on("post:create", async (payload, client) => {
130
+ server.on("jobs:create", async (payload, client) => {
83
131
  return {
84
132
  ok: true,
85
- role: client.metadata.get("role"),
133
+ role: client.metadata.get("auth.role"),
86
134
  payload
87
135
  };
88
136
  });
89
137
 
90
- const client = new SecureClient("ws://127.0.0.1:8080", {
91
- wsOptions: {
92
- headers: {
93
- "x-api-key": "dev-secret"
94
- }
95
- }
96
- });
138
+ const client = new SecureClient("ws://127.0.0.1:8080");
97
139
 
98
140
  client.on("ready", async () => {
99
- const response = await client.emit("post:create", { title: " Hello " }, { timeoutMs: 1500 });
141
+ const response = await client.emit("jobs:create", { id: "job-42" }, { timeoutMs: 1200 });
100
142
  console.log(response);
101
143
  });
102
144
  ```
103
145
 
104
- Notes:
105
-
106
- - Throwing in `connection` middleware rejects the socket and closes with code `1008`.
107
- - `metadata` is mutable in middleware (`Map`) and exposed read-only on `SecureServerClient`.
108
-
109
146
  ---
110
147
 
111
- ## Rate Limiting & DDoS Protection (0.7.1)
112
-
113
- `SecureServer` can enforce burst limits per connection and per source IP before event handlers run.
148
+ ## Rate limiting and DDoS shield
114
149
 
115
150
  ```ts
116
151
  import { SecureServer } from "@aegis-fluxion/core";
@@ -123,7 +158,7 @@ const server = new SecureServer({
123
158
  windowMs: 1_000,
124
159
  maxEventsPerConnection: 120,
125
160
  maxEventsPerIp: 300,
126
- action: "throttle",
161
+ action: "throttle", // or "disconnect"
127
162
  throttleMs: 150,
128
163
  maxThrottleMs: 2_000,
129
164
  disconnectAfterViolations: 4,
@@ -133,199 +168,17 @@ const server = new SecureServer({
133
168
  });
134
169
  ```
135
170
 
136
- Behavior summary:
137
-
138
- - When limits are exceeded, the server can **throttle** or **disconnect** the peer.
139
- - Throttle mode delays the first over-limit message and drops subsequent flood packets during the throttle window.
140
- - Disconnect mode closes abusive sockets with your configured close code/reason.
141
- - Source IP is resolved from `x-forwarded-for` (first hop) or socket remote address.
142
-
143
171
  ---
144
172
 
145
- ## MCP Adapter Integration (0.7.0)
173
+ ## Binary payload support
146
174
 
147
- Use `@aegis-fluxion/mcp-adapter` to carry MCP JSON-RPC messages through your encrypted core
148
- transport.
149
-
150
- ```ts
151
- import { SecureClient, SecureServer } from "@aegis-fluxion/core";
152
- import { SecureMCPTransport } from "@aegis-fluxion/mcp-adapter";
153
-
154
- const secureServer = new SecureServer({ host: "127.0.0.1", port: 9091 });
155
-
156
- secureServer.on("connection", async (client) => {
157
- const mcpServerTransport = new SecureMCPTransport({
158
- mode: "server",
159
- server: secureServer,
160
- clientId: client.id
161
- });
162
-
163
- mcpServerTransport.onmessage = async (message) => {
164
- // Forward into your MCP server runtime.
165
- console.log("MCP request on server tunnel", message);
166
- };
167
-
168
- await mcpServerTransport.start();
169
- });
170
-
171
- const secureClient = new SecureClient("ws://127.0.0.1:9091");
172
-
173
- const mcpClientTransport = new SecureMCPTransport({
174
- mode: "client",
175
- client: secureClient
176
- });
177
-
178
- await mcpClientTransport.start();
179
-
180
- await mcpClientTransport.send({
181
- jsonrpc: "2.0",
182
- id: 100,
183
- method: "tools/list",
184
- params: {}
185
- });
186
- ```
187
-
188
- ---
189
-
190
- ## Binary Data Support
191
-
192
- `@aegis-fluxion/core` supports encrypted binary payload transfer while preserving type fidelity.
193
-
194
- Supported send/receive types:
175
+ Supported encrypted payload types:
195
176
 
196
177
  - `Buffer`
197
178
  - `Uint8Array`
198
179
  - `Blob`
199
180
 
200
- Binary values can be nested in regular objects and arrays.
201
-
202
- ---
203
-
204
- ## Example: Encrypted Binary Event
205
-
206
- ```ts
207
- import { SecureClient, SecureServer } from "@aegis-fluxion/core";
208
-
209
- const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
210
- const client = new SecureClient("ws://127.0.0.1:8080");
211
-
212
- server.on("image:chunk", (data, socket) => {
213
- const chunk = data as Buffer;
214
-
215
- if (!Buffer.isBuffer(chunk)) {
216
- throw new Error("Expected Buffer payload.");
217
- }
218
-
219
- socket.emit("image:chunk:ack", chunk);
220
- });
221
-
222
- client.on("ready", () => {
223
- const imageChunk = Buffer.from("89504e470d0a", "hex");
224
- client.emit("image:chunk", imageChunk);
225
- });
226
-
227
- client.on("image:chunk:ack", (payload) => {
228
- const echoedChunk = payload as Buffer;
229
- console.log("Echoed bytes:", echoedChunk.byteLength);
230
- });
231
- ```
232
-
233
- ---
234
-
235
- ## Example: ACK Roundtrip with Mixed Binary Types
236
-
237
- ```ts
238
- server.on("binary:inspect", async (payload) => {
239
- const { file, bytes, blob } = payload as {
240
- file: Buffer;
241
- bytes: Uint8Array;
242
- blob: Blob;
243
- };
244
-
245
- return {
246
- fileBytes: file.byteLength,
247
- bytesBytes: bytes.byteLength,
248
- blobBytes: blob.size
249
- };
250
- });
251
-
252
- client.on("ready", async () => {
253
- const result = await client.emit(
254
- "binary:inspect",
255
- {
256
- file: Buffer.from("file-binary"),
257
- bytes: Uint8Array.from([1, 2, 3, 4]),
258
- blob: new Blob([Buffer.from("blob-binary")], {
259
- type: "application/octet-stream"
260
- })
261
- },
262
- { timeoutMs: 1500 }
263
- );
264
-
265
- console.log(result);
266
- });
267
- ```
268
-
269
- ---
270
-
271
- ## API Snapshot
272
-
273
- ### `SecureServer`
274
-
275
- - `on("connection" | "ready" | "disconnect" | "error", handler)`
276
- - `on("custom:event", (data, client) => unknown | Promise<unknown>)`
277
- - `use((context, next) => void | Promise<void>)`
278
- - `emit(event, data): SecureServer`
279
- - `emitTo(clientId, event, data): boolean`
280
- - `emitTo(clientId, event, data, callback): boolean`
281
- - `emitTo(clientId, event, data, options): Promise<unknown>`
282
- - `emitTo(clientId, event, data, options, callback): boolean`
283
- - `to(room).emit(event, data): SecureServer`
284
- - `close(code?, reason?): void`
285
-
286
- ### `SecureServerClient`
287
-
288
- - `id: string`
289
- - `socket: WebSocket`
290
- - `metadata: ReadonlyMap<string, unknown>`
291
- - `emit(event, data, ...ackArgs): boolean | Promise<unknown>`
292
- - `join(room): boolean`
293
- - `leave(room): boolean`
294
- - `leaveAll(): number`
295
-
296
- ### Middleware Types
297
-
298
- - `SecureServerMiddleware`
299
- - `SecureServerMiddlewareContext`
300
- - `SecureServerConnectionMiddlewareContext`
301
- - `SecureServerMessageMiddlewareContext`
302
- - `SecureServerMiddlewareNext`
303
- - `SecureServerRateLimitOptions`
304
- - `SecureServerRateLimitAction`
305
-
306
- ### `SecureClient`
307
-
308
- - `connect(): void`
309
- - `disconnect(code?, reason?): void`
310
- - `isConnected(): boolean`
311
- - `readyState: number | null`
312
- - `emit(event, data): boolean`
313
- - `emit(event, data, callback): boolean`
314
- - `emit(event, data, options): Promise<unknown>`
315
- - `emit(event, data, options, callback): boolean`
316
- - `on("connect" | "ready" | "disconnect" | "error", handler)`
317
- - `on("custom:event", handler)`
318
-
319
- ---
320
-
321
- ## Security Notes
322
-
323
- - All payloads (including binary) are encrypted end-to-end with AES-256-GCM.
324
- - Authentication tags are verified on every packet (tampered packets are dropped).
325
- - Internal transport events are reserved (`__handshake`, `__rpc:req`, `__rpc:res`).
326
- - Pending ACK requests are rejected on timeout/disconnect.
327
- - Overload traffic can be throttled or disconnected before custom handlers are invoked.
328
- - Middleware-level policy rejection uses WebSocket close code `1008`.
181
+ Binary fields can be nested in standard JSON objects and arrays.
329
182
 
330
183
  ---
331
184
 
package/dist/index.cjs CHANGED
@@ -40,6 +40,51 @@ var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
40
40
  var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
41
41
  var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
42
42
  var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
43
+ var SECURE_SERVER_ADAPTER_MESSAGE_VERSION = 1;
44
+ function normalizeSecureServerAdapterMessage(value) {
45
+ if (!isPlainObject(value)) {
46
+ throw new Error("SecureServer adapter message must be a plain object.");
47
+ }
48
+ if (value.version !== SECURE_SERVER_ADAPTER_MESSAGE_VERSION) {
49
+ throw new Error(
50
+ `Unsupported SecureServer adapter message version: ${String(value.version)}.`
51
+ );
52
+ }
53
+ if (typeof value.originServerId !== "string" || value.originServerId.trim().length === 0) {
54
+ throw new Error("SecureServer adapter message originServerId must be a non-empty string.");
55
+ }
56
+ if (value.scope !== "broadcast" && value.scope !== "room") {
57
+ throw new Error('SecureServer adapter message scope must be either "broadcast" or "room".');
58
+ }
59
+ if (typeof value.event !== "string" || value.event.trim().length === 0) {
60
+ throw new Error("SecureServer adapter message event must be a non-empty string.");
61
+ }
62
+ if (typeof value.emittedAt !== "number" || !Number.isFinite(value.emittedAt)) {
63
+ throw new Error("SecureServer adapter message emittedAt must be a finite number.");
64
+ }
65
+ if (value.scope === "room") {
66
+ if (typeof value.room !== "string" || value.room.trim().length === 0) {
67
+ throw new Error("SecureServer adapter message room must be a non-empty string.");
68
+ }
69
+ return {
70
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
71
+ originServerId: value.originServerId,
72
+ scope: value.scope,
73
+ event: value.event,
74
+ data: value.data,
75
+ emittedAt: value.emittedAt,
76
+ room: value.room.trim()
77
+ };
78
+ }
79
+ return {
80
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
81
+ originServerId: value.originServerId,
82
+ scope: value.scope,
83
+ event: value.event,
84
+ data: value.data,
85
+ emittedAt: value.emittedAt
86
+ };
87
+ }
43
88
  function normalizeToError(error, fallbackMessage) {
44
89
  if (error instanceof Error) {
45
90
  return error;
@@ -368,7 +413,9 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
368
413
  return plaintext.toString("utf8");
369
414
  }
370
415
  var SecureServer = class {
416
+ instanceId = crypto.randomUUID();
371
417
  socketServer;
418
+ adapter = null;
372
419
  heartbeatConfig;
373
420
  rateLimitConfig;
374
421
  heartbeatIntervalHandle = null;
@@ -393,19 +440,76 @@ var SecureServer = class {
393
440
  rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
394
441
  rateLimitBucketsByIp = /* @__PURE__ */ new Map();
395
442
  constructor(options) {
396
- const { heartbeat, rateLimit, ...socketServerOptions } = options;
443
+ const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
397
444
  this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
398
445
  this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
399
446
  this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
400
447
  this.bindSocketServerEvents();
401
448
  this.startHeartbeatLoop();
449
+ if (adapter) {
450
+ void this.setAdapter(adapter).catch(() => {
451
+ return void 0;
452
+ });
453
+ }
402
454
  }
403
455
  get clientCount() {
404
456
  return this.clientsById.size;
405
457
  }
458
+ get serverId() {
459
+ return this.instanceId;
460
+ }
406
461
  get clients() {
407
462
  return this.clientsById;
408
463
  }
464
+ async setAdapter(adapter) {
465
+ const previousAdapter = this.adapter;
466
+ if (previousAdapter === adapter) {
467
+ return;
468
+ }
469
+ try {
470
+ if (previousAdapter?.detach) {
471
+ await Promise.resolve(previousAdapter.detach(this));
472
+ }
473
+ this.adapter = null;
474
+ if (!adapter) {
475
+ return;
476
+ }
477
+ await Promise.resolve(adapter.attach(this));
478
+ this.adapter = adapter;
479
+ } catch (error) {
480
+ const normalizedError = normalizeToError(
481
+ error,
482
+ "Failed to set SecureServer adapter."
483
+ );
484
+ this.notifyError(normalizedError);
485
+ throw normalizedError;
486
+ }
487
+ }
488
+ async handleAdapterMessage(message) {
489
+ try {
490
+ const normalizedMessage = normalizeSecureServerAdapterMessage(message);
491
+ if (normalizedMessage.originServerId === this.instanceId) {
492
+ return;
493
+ }
494
+ if (normalizedMessage.scope === "broadcast") {
495
+ this.emitLocally(normalizedMessage.event, normalizedMessage.data);
496
+ return;
497
+ }
498
+ if (!normalizedMessage.room) {
499
+ return;
500
+ }
501
+ this.emitToRoom(
502
+ normalizedMessage.room,
503
+ normalizedMessage.event,
504
+ normalizedMessage.data,
505
+ false
506
+ );
507
+ } catch (error) {
508
+ this.notifyError(
509
+ normalizeToError(error, "Failed to process SecureServer adapter message.")
510
+ );
511
+ }
512
+ }
409
513
  on(event, handler) {
410
514
  try {
411
515
  if (event === "connection") {
@@ -492,12 +596,12 @@ var SecureServer = class {
492
596
  if (isReservedEmitEvent(event)) {
493
597
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
494
598
  }
495
- const envelope = { event, data };
496
- for (const client of this.clientsById.values()) {
497
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
498
- return void 0;
499
- });
500
- }
599
+ this.emitLocally(event, data);
600
+ this.publishAdapterMessage({
601
+ scope: "broadcast",
602
+ event,
603
+ data
604
+ });
501
605
  } catch (error) {
502
606
  this.notifyError(normalizeToError(error, "Failed to emit server event."));
503
607
  }
@@ -554,7 +658,7 @@ var SecureServer = class {
554
658
  return {
555
659
  emit: (event, data) => {
556
660
  try {
557
- this.emitToRoom(normalizedRoom, event, data);
661
+ this.emitToRoom(normalizedRoom, event, data, true);
558
662
  } catch (error) {
559
663
  this.notifyError(
560
664
  normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
@@ -567,6 +671,15 @@ var SecureServer = class {
567
671
  close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
568
672
  try {
569
673
  this.stopHeartbeatLoop();
674
+ const activeAdapter = this.adapter;
675
+ this.adapter = null;
676
+ if (activeAdapter?.detach) {
677
+ void Promise.resolve(activeAdapter.detach(this)).catch((error) => {
678
+ this.notifyError(
679
+ normalizeToError(error, "Failed to detach SecureServer adapter during close.")
680
+ );
681
+ });
682
+ }
570
683
  for (const client of this.clientsById.values()) {
571
684
  this.rejectPendingRpcRequests(
572
685
  client.socket,
@@ -1435,6 +1548,48 @@ var SecureServer = class {
1435
1548
  leaveAll: () => this.leaveClientFromAllRooms(clientId)
1436
1549
  };
1437
1550
  }
1551
+ emitLocally(event, data) {
1552
+ const envelope = { event, data };
1553
+ for (const client of this.clientsById.values()) {
1554
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1555
+ return void 0;
1556
+ });
1557
+ }
1558
+ }
1559
+ publishAdapterMessage(message) {
1560
+ if (!this.adapter) {
1561
+ return;
1562
+ }
1563
+ let adapterMessage;
1564
+ if (message.scope === "room") {
1565
+ if (!message.room) {
1566
+ return;
1567
+ }
1568
+ adapterMessage = {
1569
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
1570
+ originServerId: this.instanceId,
1571
+ scope: "room",
1572
+ event: message.event,
1573
+ data: message.data,
1574
+ emittedAt: Date.now(),
1575
+ room: message.room
1576
+ };
1577
+ } else {
1578
+ adapterMessage = {
1579
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
1580
+ originServerId: this.instanceId,
1581
+ scope: "broadcast",
1582
+ event: message.event,
1583
+ data: message.data,
1584
+ emittedAt: Date.now()
1585
+ };
1586
+ }
1587
+ void Promise.resolve(this.adapter.publish(adapterMessage)).catch((error) => {
1588
+ this.notifyError(
1589
+ normalizeToError(error, "Failed to publish SecureServer adapter message.")
1590
+ );
1591
+ });
1592
+ }
1438
1593
  normalizeRoomName(room) {
1439
1594
  if (typeof room !== "string") {
1440
1595
  throw new Error("Room name must be a string.");
@@ -1500,22 +1655,29 @@ var SecureServer = class {
1500
1655
  }
1501
1656
  return roomNames.length;
1502
1657
  }
1503
- emitToRoom(room, event, data) {
1658
+ emitToRoom(room, event, data, replicate) {
1504
1659
  if (isReservedEmitEvent(event)) {
1505
1660
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
1506
1661
  }
1507
1662
  const roomMembers = this.roomMembersByName.get(room);
1508
- if (!roomMembers || roomMembers.size === 0) {
1509
- return;
1510
- }
1511
- const envelope = { event, data };
1512
- for (const clientId of roomMembers) {
1513
- const client = this.clientsById.get(clientId);
1514
- if (!client) {
1515
- continue;
1663
+ if (roomMembers && roomMembers.size > 0) {
1664
+ const envelope = { event, data };
1665
+ for (const clientId of roomMembers) {
1666
+ const client = this.clientsById.get(clientId);
1667
+ if (!client) {
1668
+ continue;
1669
+ }
1670
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1671
+ return void 0;
1672
+ });
1516
1673
  }
1517
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1518
- return void 0;
1674
+ }
1675
+ if (replicate) {
1676
+ this.publishAdapterMessage({
1677
+ scope: "room",
1678
+ room,
1679
+ event,
1680
+ data
1519
1681
  });
1520
1682
  }
1521
1683
  }
@@ -2120,5 +2282,6 @@ var SecureClient = class {
2120
2282
 
2121
2283
  exports.SecureClient = SecureClient;
2122
2284
  exports.SecureServer = SecureServer;
2285
+ exports.normalizeSecureServerAdapterMessage = normalizeSecureServerAdapterMessage;
2123
2286
  //# sourceMappingURL=index.cjs.map
2124
2287
  //# sourceMappingURL=index.cjs.map