@aegis-fluxion/core 0.7.0 → 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/dist/index.cjs CHANGED
@@ -32,6 +32,59 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
32
32
  var DEFAULT_RECONNECT_FACTOR = 2;
33
33
  var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
34
34
  var DEFAULT_RPC_TIMEOUT_MS = 5e3;
35
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 1e3;
36
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION = 120;
37
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP = 300;
38
+ var DEFAULT_RATE_LIMIT_THROTTLE_MS = 150;
39
+ var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
40
+ var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
41
+ var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
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
+ }
35
88
  function normalizeToError(error, fallbackMessage) {
36
89
  if (error instanceof Error) {
37
90
  return error;
@@ -65,6 +118,11 @@ function rawDataToBuffer(rawData) {
65
118
  }
66
119
  return Buffer.from(rawData);
67
120
  }
121
+ function delay(ms) {
122
+ return new Promise((resolve) => {
123
+ setTimeout(resolve, ms);
124
+ });
125
+ }
68
126
  function isBlobValue(value) {
69
127
  return typeof Blob !== "undefined" && value instanceof Blob;
70
128
  }
@@ -355,8 +413,11 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
355
413
  return plaintext.toString("utf8");
356
414
  }
357
415
  var SecureServer = class {
416
+ instanceId = crypto.randomUUID();
358
417
  socketServer;
418
+ adapter = null;
359
419
  heartbeatConfig;
420
+ rateLimitConfig;
360
421
  heartbeatIntervalHandle = null;
361
422
  clientsById = /* @__PURE__ */ new Map();
362
423
  clientIdBySocket = /* @__PURE__ */ new Map();
@@ -375,19 +436,80 @@ var SecureServer = class {
375
436
  heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
376
437
  roomMembersByName = /* @__PURE__ */ new Map();
377
438
  roomNamesByClientId = /* @__PURE__ */ new Map();
439
+ clientIpByClientId = /* @__PURE__ */ new Map();
440
+ rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
441
+ rateLimitBucketsByIp = /* @__PURE__ */ new Map();
378
442
  constructor(options) {
379
- const { heartbeat, ...socketServerOptions } = options;
443
+ const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
380
444
  this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
445
+ this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
381
446
  this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
382
447
  this.bindSocketServerEvents();
383
448
  this.startHeartbeatLoop();
449
+ if (adapter) {
450
+ void this.setAdapter(adapter).catch(() => {
451
+ return void 0;
452
+ });
453
+ }
384
454
  }
385
455
  get clientCount() {
386
456
  return this.clientsById.size;
387
457
  }
458
+ get serverId() {
459
+ return this.instanceId;
460
+ }
388
461
  get clients() {
389
462
  return this.clientsById;
390
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
+ }
391
513
  on(event, handler) {
392
514
  try {
393
515
  if (event === "connection") {
@@ -474,12 +596,12 @@ var SecureServer = class {
474
596
  if (isReservedEmitEvent(event)) {
475
597
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
476
598
  }
477
- const envelope = { event, data };
478
- for (const client of this.clientsById.values()) {
479
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
480
- return void 0;
481
- });
482
- }
599
+ this.emitLocally(event, data);
600
+ this.publishAdapterMessage({
601
+ scope: "broadcast",
602
+ event,
603
+ data
604
+ });
483
605
  } catch (error) {
484
606
  this.notifyError(normalizeToError(error, "Failed to emit server event."));
485
607
  }
@@ -536,7 +658,7 @@ var SecureServer = class {
536
658
  return {
537
659
  emit: (event, data) => {
538
660
  try {
539
- this.emitToRoom(normalizedRoom, event, data);
661
+ this.emitToRoom(normalizedRoom, event, data, true);
540
662
  } catch (error) {
541
663
  this.notifyError(
542
664
  normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
@@ -549,6 +671,15 @@ var SecureServer = class {
549
671
  close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
550
672
  try {
551
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
+ }
552
683
  for (const client of this.clientsById.values()) {
553
684
  this.rejectPendingRpcRequests(
554
685
  client.socket,
@@ -559,6 +690,9 @@ var SecureServer = class {
559
690
  client.socket.close(code, reason);
560
691
  }
561
692
  }
693
+ this.rateLimitBucketsByClientId.clear();
694
+ this.rateLimitBucketsByIp.clear();
695
+ this.clientIpByClientId.clear();
562
696
  this.socketServer.close();
563
697
  } catch (error) {
564
698
  this.notifyError(normalizeToError(error, "Failed to close server."));
@@ -579,6 +713,211 @@ var SecureServer = class {
579
713
  timeoutMs
580
714
  };
581
715
  }
716
+ resolveRateLimitConfig(rateLimitOptions) {
717
+ const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
718
+ const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
719
+ const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
720
+ const action = rateLimitOptions?.action ?? "throttle";
721
+ const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
722
+ const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
723
+ const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
724
+ const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
725
+ const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
726
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
727
+ throw new Error("Server rateLimit windowMs must be a positive number.");
728
+ }
729
+ if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
730
+ throw new Error(
731
+ "Server rateLimit maxEventsPerConnection must be a positive number."
732
+ );
733
+ }
734
+ if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
735
+ throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
736
+ }
737
+ if (action !== "throttle" && action !== "disconnect") {
738
+ throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
739
+ }
740
+ if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
741
+ throw new Error("Server rateLimit throttleMs must be a positive number.");
742
+ }
743
+ if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
744
+ throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
745
+ }
746
+ if (maxThrottleMs < throttleMs) {
747
+ throw new Error(
748
+ "Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
749
+ );
750
+ }
751
+ if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
752
+ throw new Error(
753
+ "Server rateLimit disconnectAfterViolations must be a positive integer."
754
+ );
755
+ }
756
+ if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
757
+ throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
758
+ }
759
+ return {
760
+ enabled: rateLimitOptions?.enabled ?? true,
761
+ windowMs,
762
+ maxEventsPerConnection,
763
+ maxEventsPerIp,
764
+ action,
765
+ throttleMs,
766
+ maxThrottleMs,
767
+ disconnectAfterViolations,
768
+ disconnectCode,
769
+ disconnectReason
770
+ };
771
+ }
772
+ createRateLimitBucket(now) {
773
+ return {
774
+ windowStartedAt: now,
775
+ count: 0,
776
+ violationCount: 0,
777
+ throttleUntil: 0,
778
+ lastSeenAt: now
779
+ };
780
+ }
781
+ getOrCreateRateLimitBucket(map, key, now) {
782
+ const existingBucket = map.get(key);
783
+ if (existingBucket) {
784
+ return existingBucket;
785
+ }
786
+ const bucket = this.createRateLimitBucket(now);
787
+ map.set(key, bucket);
788
+ return bucket;
789
+ }
790
+ updateRateLimitBucket(bucket, now) {
791
+ if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
792
+ bucket.windowStartedAt = now;
793
+ bucket.count = 0;
794
+ bucket.violationCount = 0;
795
+ bucket.throttleUntil = 0;
796
+ }
797
+ bucket.count += 1;
798
+ bucket.lastSeenAt = now;
799
+ }
800
+ pruneRateLimitBucketMap(map, now, maxIdleMs) {
801
+ for (const [key, bucket] of map.entries()) {
802
+ if (now - bucket.lastSeenAt >= maxIdleMs) {
803
+ map.delete(key);
804
+ }
805
+ }
806
+ }
807
+ pruneRateLimitBuckets(now) {
808
+ const maxIdleMs = this.rateLimitConfig.windowMs * 4;
809
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
810
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
811
+ }
812
+ normalizeIpAddress(ipAddress) {
813
+ let normalized = ipAddress.trim().toLowerCase();
814
+ if (normalized.startsWith("::ffff:")) {
815
+ normalized = normalized.slice(7);
816
+ }
817
+ if (normalized.startsWith("[") && normalized.endsWith("]")) {
818
+ normalized = normalized.slice(1, -1);
819
+ }
820
+ const zoneIndex = normalized.indexOf("%");
821
+ if (zoneIndex >= 0) {
822
+ normalized = normalized.slice(0, zoneIndex);
823
+ }
824
+ return normalized.length > 0 ? normalized : "unknown";
825
+ }
826
+ resolveClientIp(request) {
827
+ const forwardedHeader = request.headers["x-forwarded-for"];
828
+ const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
829
+ if (typeof forwardedValue === "string") {
830
+ const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
831
+ if (firstForwardedIp) {
832
+ return this.normalizeIpAddress(firstForwardedIp);
833
+ }
834
+ }
835
+ return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
836
+ }
837
+ isIpStillConnected(ipAddress) {
838
+ for (const connectedIp of this.clientIpByClientId.values()) {
839
+ if (connectedIp === ipAddress) {
840
+ return true;
841
+ }
842
+ }
843
+ return false;
844
+ }
845
+ evaluateIncomingRateLimit(client) {
846
+ const noLimitDecision = {
847
+ shouldDisconnect: false,
848
+ shouldDrop: false,
849
+ throttleDelayMs: 0
850
+ };
851
+ if (!this.rateLimitConfig.enabled) {
852
+ return noLimitDecision;
853
+ }
854
+ const now = Date.now();
855
+ const clientBucket = this.getOrCreateRateLimitBucket(
856
+ this.rateLimitBucketsByClientId,
857
+ client.id,
858
+ now
859
+ );
860
+ this.updateRateLimitBucket(clientBucket, now);
861
+ const clientIp = this.clientIpByClientId.get(client.id);
862
+ const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
863
+ if (ipBucket) {
864
+ this.updateRateLimitBucket(ipBucket, now);
865
+ }
866
+ const activeThrottleUntil = Math.max(
867
+ clientBucket.throttleUntil,
868
+ ipBucket?.throttleUntil ?? 0
869
+ );
870
+ if (activeThrottleUntil > now) {
871
+ return {
872
+ shouldDisconnect: false,
873
+ shouldDrop: true,
874
+ throttleDelayMs: 0
875
+ };
876
+ }
877
+ const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
878
+ const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
879
+ if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
880
+ if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
881
+ this.pruneRateLimitBuckets(now);
882
+ }
883
+ return noLimitDecision;
884
+ }
885
+ if (isConnectionLimitExceeded) {
886
+ clientBucket.violationCount += 1;
887
+ }
888
+ if (ipBucket && isIpLimitExceeded) {
889
+ ipBucket.violationCount += 1;
890
+ }
891
+ const violationCount = Math.max(
892
+ clientBucket.violationCount,
893
+ ipBucket?.violationCount ?? 0
894
+ );
895
+ const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
896
+ if (shouldDisconnect) {
897
+ return {
898
+ shouldDisconnect: true,
899
+ shouldDrop: true,
900
+ throttleDelayMs: 0
901
+ };
902
+ }
903
+ const throttleDelayMs = Math.min(
904
+ this.rateLimitConfig.maxThrottleMs,
905
+ Math.max(
906
+ this.rateLimitConfig.throttleMs,
907
+ this.rateLimitConfig.throttleMs * violationCount
908
+ )
909
+ );
910
+ const throttleUntil = now + throttleDelayMs;
911
+ clientBucket.throttleUntil = throttleUntil;
912
+ if (ipBucket) {
913
+ ipBucket.throttleUntil = throttleUntil;
914
+ }
915
+ return {
916
+ shouldDisconnect: false,
917
+ shouldDrop: false,
918
+ throttleDelayMs
919
+ };
920
+ }
582
921
  startHeartbeatLoop() {
583
922
  if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
584
923
  return;
@@ -681,6 +1020,8 @@ var SecureServer = class {
681
1020
  try {
682
1021
  const clientId = crypto.randomUUID();
683
1022
  const handshakeState = this.createServerHandshakeState();
1023
+ const clientIp = this.resolveClientIp(request);
1024
+ connectionMetadata.set("network.ip", clientIp);
684
1025
  const client = this.createSecureServerClient(
685
1026
  clientId,
686
1027
  socket,
@@ -689,6 +1030,7 @@ var SecureServer = class {
689
1030
  );
690
1031
  this.clientsById.set(clientId, client);
691
1032
  this.clientIdBySocket.set(socket, clientId);
1033
+ this.clientIpByClientId.set(clientId, clientIp);
692
1034
  this.handshakeStateBySocket.set(socket, handshakeState);
693
1035
  this.pendingPayloadsBySocket.set(socket, []);
694
1036
  this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
@@ -723,6 +1065,35 @@ var SecureServer = class {
723
1065
  }
724
1066
  async handleIncomingMessage(client, rawData) {
725
1067
  try {
1068
+ const rateLimitDecision = this.evaluateIncomingRateLimit(client);
1069
+ if (rateLimitDecision.shouldDisconnect) {
1070
+ this.notifyError(
1071
+ new Error(
1072
+ `Rate limit disconnect triggered for client ${client.id}.`
1073
+ )
1074
+ );
1075
+ if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
1076
+ client.socket.close(
1077
+ this.rateLimitConfig.disconnectCode,
1078
+ this.rateLimitConfig.disconnectReason
1079
+ );
1080
+ }
1081
+ return;
1082
+ }
1083
+ if (rateLimitDecision.shouldDrop) {
1084
+ return;
1085
+ }
1086
+ if (rateLimitDecision.throttleDelayMs > 0) {
1087
+ this.notifyError(
1088
+ new Error(
1089
+ `Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
1090
+ )
1091
+ );
1092
+ await delay(rateLimitDecision.throttleDelayMs);
1093
+ if (client.socket.readyState !== WebSocket__default.default.OPEN) {
1094
+ return;
1095
+ }
1096
+ }
726
1097
  let envelope = null;
727
1098
  try {
728
1099
  envelope = parseEnvelope(rawData);
@@ -784,6 +1155,12 @@ var SecureServer = class {
784
1155
  client.leaveAll();
785
1156
  this.clientsById.delete(client.id);
786
1157
  this.clientIdBySocket.delete(client.socket);
1158
+ const disconnectedIp = this.clientIpByClientId.get(client.id);
1159
+ this.clientIpByClientId.delete(client.id);
1160
+ this.rateLimitBucketsByClientId.delete(client.id);
1161
+ if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
1162
+ this.rateLimitBucketsByIp.delete(disconnectedIp);
1163
+ }
787
1164
  this.handshakeStateBySocket.delete(client.socket);
788
1165
  this.sharedSecretBySocket.delete(client.socket);
789
1166
  this.encryptionKeyBySocket.delete(client.socket);
@@ -1171,6 +1548,48 @@ var SecureServer = class {
1171
1548
  leaveAll: () => this.leaveClientFromAllRooms(clientId)
1172
1549
  };
1173
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
+ }
1174
1593
  normalizeRoomName(room) {
1175
1594
  if (typeof room !== "string") {
1176
1595
  throw new Error("Room name must be a string.");
@@ -1236,22 +1655,29 @@ var SecureServer = class {
1236
1655
  }
1237
1656
  return roomNames.length;
1238
1657
  }
1239
- emitToRoom(room, event, data) {
1658
+ emitToRoom(room, event, data, replicate) {
1240
1659
  if (isReservedEmitEvent(event)) {
1241
1660
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
1242
1661
  }
1243
1662
  const roomMembers = this.roomMembersByName.get(room);
1244
- if (!roomMembers || roomMembers.size === 0) {
1245
- return;
1246
- }
1247
- const envelope = { event, data };
1248
- for (const clientId of roomMembers) {
1249
- const client = this.clientsById.get(clientId);
1250
- if (!client) {
1251
- 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
+ });
1252
1673
  }
1253
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1254
- return void 0;
1674
+ }
1675
+ if (replicate) {
1676
+ this.publishAdapterMessage({
1677
+ scope: "room",
1678
+ room,
1679
+ event,
1680
+ data
1255
1681
  });
1256
1682
  }
1257
1683
  }
@@ -1856,5 +2282,6 @@ var SecureClient = class {
1856
2282
 
1857
2283
  exports.SecureClient = SecureClient;
1858
2284
  exports.SecureServer = SecureServer;
2285
+ exports.normalizeSecureServerAdapterMessage = normalizeSecureServerAdapterMessage;
1859
2286
  //# sourceMappingURL=index.cjs.map
1860
2287
  //# sourceMappingURL=index.cjs.map