@aegis-fluxion/core 0.7.0 → 0.7.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/dist/index.d.cts CHANGED
@@ -15,8 +15,22 @@ interface SecureServerHeartbeatOptions {
15
15
  intervalMs?: number;
16
16
  timeoutMs?: number;
17
17
  }
18
+ type SecureServerRateLimitAction = "throttle" | "disconnect";
19
+ interface SecureServerRateLimitOptions {
20
+ enabled?: boolean;
21
+ windowMs?: number;
22
+ maxEventsPerConnection?: number;
23
+ maxEventsPerIp?: number;
24
+ action?: SecureServerRateLimitAction;
25
+ throttleMs?: number;
26
+ maxThrottleMs?: number;
27
+ disconnectAfterViolations?: number;
28
+ disconnectCode?: number;
29
+ disconnectReason?: string;
30
+ }
18
31
  interface SecureServerOptions extends ServerOptions {
19
32
  heartbeat?: SecureServerHeartbeatOptions;
33
+ rateLimit?: SecureServerRateLimitOptions;
20
34
  }
21
35
  interface SecureClientReconnectOptions {
22
36
  enabled?: boolean;
@@ -87,6 +101,7 @@ type SecureServerMiddleware = (context: SecureServerMiddlewareContext, next: Sec
87
101
  declare class SecureServer {
88
102
  private readonly socketServer;
89
103
  private readonly heartbeatConfig;
104
+ private readonly rateLimitConfig;
90
105
  private heartbeatIntervalHandle;
91
106
  private readonly clientsById;
92
107
  private readonly clientIdBySocket;
@@ -105,6 +120,9 @@ declare class SecureServer {
105
120
  private readonly heartbeatStateBySocket;
106
121
  private readonly roomMembersByName;
107
122
  private readonly roomNamesByClientId;
123
+ private readonly clientIpByClientId;
124
+ private readonly rateLimitBucketsByClientId;
125
+ private readonly rateLimitBucketsByIp;
108
126
  constructor(options: SecureServerOptions);
109
127
  get clientCount(): number;
110
128
  get clients(): ReadonlyMap<string, SecureServerClient>;
@@ -127,6 +145,16 @@ declare class SecureServer {
127
145
  to(room: string): SecureServerRoomOperator;
128
146
  close(code?: number, reason?: string): void;
129
147
  private resolveHeartbeatConfig;
148
+ private resolveRateLimitConfig;
149
+ private createRateLimitBucket;
150
+ private getOrCreateRateLimitBucket;
151
+ private updateRateLimitBucket;
152
+ private pruneRateLimitBucketMap;
153
+ private pruneRateLimitBuckets;
154
+ private normalizeIpAddress;
155
+ private resolveClientIp;
156
+ private isIpStillConnected;
157
+ private evaluateIncomingRateLimit;
130
158
  private startHeartbeatLoop;
131
159
  private stopHeartbeatLoop;
132
160
  private performHeartbeatSweep;
@@ -223,4 +251,4 @@ declare class SecureClient {
223
251
  private flushPendingPayloadQueue;
224
252
  }
225
253
 
226
- export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
254
+ export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerRateLimitAction, type SecureServerRateLimitOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
package/dist/index.d.ts CHANGED
@@ -15,8 +15,22 @@ interface SecureServerHeartbeatOptions {
15
15
  intervalMs?: number;
16
16
  timeoutMs?: number;
17
17
  }
18
+ type SecureServerRateLimitAction = "throttle" | "disconnect";
19
+ interface SecureServerRateLimitOptions {
20
+ enabled?: boolean;
21
+ windowMs?: number;
22
+ maxEventsPerConnection?: number;
23
+ maxEventsPerIp?: number;
24
+ action?: SecureServerRateLimitAction;
25
+ throttleMs?: number;
26
+ maxThrottleMs?: number;
27
+ disconnectAfterViolations?: number;
28
+ disconnectCode?: number;
29
+ disconnectReason?: string;
30
+ }
18
31
  interface SecureServerOptions extends ServerOptions {
19
32
  heartbeat?: SecureServerHeartbeatOptions;
33
+ rateLimit?: SecureServerRateLimitOptions;
20
34
  }
21
35
  interface SecureClientReconnectOptions {
22
36
  enabled?: boolean;
@@ -87,6 +101,7 @@ type SecureServerMiddleware = (context: SecureServerMiddlewareContext, next: Sec
87
101
  declare class SecureServer {
88
102
  private readonly socketServer;
89
103
  private readonly heartbeatConfig;
104
+ private readonly rateLimitConfig;
90
105
  private heartbeatIntervalHandle;
91
106
  private readonly clientsById;
92
107
  private readonly clientIdBySocket;
@@ -105,6 +120,9 @@ declare class SecureServer {
105
120
  private readonly heartbeatStateBySocket;
106
121
  private readonly roomMembersByName;
107
122
  private readonly roomNamesByClientId;
123
+ private readonly clientIpByClientId;
124
+ private readonly rateLimitBucketsByClientId;
125
+ private readonly rateLimitBucketsByIp;
108
126
  constructor(options: SecureServerOptions);
109
127
  get clientCount(): number;
110
128
  get clients(): ReadonlyMap<string, SecureServerClient>;
@@ -127,6 +145,16 @@ declare class SecureServer {
127
145
  to(room: string): SecureServerRoomOperator;
128
146
  close(code?: number, reason?: string): void;
129
147
  private resolveHeartbeatConfig;
148
+ private resolveRateLimitConfig;
149
+ private createRateLimitBucket;
150
+ private getOrCreateRateLimitBucket;
151
+ private updateRateLimitBucket;
152
+ private pruneRateLimitBucketMap;
153
+ private pruneRateLimitBuckets;
154
+ private normalizeIpAddress;
155
+ private resolveClientIp;
156
+ private isIpStillConnected;
157
+ private evaluateIncomingRateLimit;
130
158
  private startHeartbeatLoop;
131
159
  private stopHeartbeatLoop;
132
160
  private performHeartbeatSweep;
@@ -223,4 +251,4 @@ declare class SecureClient {
223
251
  private flushPendingPayloadQueue;
224
252
  }
225
253
 
226
- export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
254
+ export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerRateLimitAction, type SecureServerRateLimitOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
package/dist/index.js CHANGED
@@ -26,6 +26,14 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
26
26
  var DEFAULT_RECONNECT_FACTOR = 2;
27
27
  var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
28
28
  var DEFAULT_RPC_TIMEOUT_MS = 5e3;
29
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 1e3;
30
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION = 120;
31
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP = 300;
32
+ var DEFAULT_RATE_LIMIT_THROTTLE_MS = 150;
33
+ var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
34
+ var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
35
+ var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
36
+ var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
29
37
  function normalizeToError(error, fallbackMessage) {
30
38
  if (error instanceof Error) {
31
39
  return error;
@@ -59,6 +67,11 @@ function rawDataToBuffer(rawData) {
59
67
  }
60
68
  return Buffer.from(rawData);
61
69
  }
70
+ function delay(ms) {
71
+ return new Promise((resolve) => {
72
+ setTimeout(resolve, ms);
73
+ });
74
+ }
62
75
  function isBlobValue(value) {
63
76
  return typeof Blob !== "undefined" && value instanceof Blob;
64
77
  }
@@ -351,6 +364,7 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
351
364
  var SecureServer = class {
352
365
  socketServer;
353
366
  heartbeatConfig;
367
+ rateLimitConfig;
354
368
  heartbeatIntervalHandle = null;
355
369
  clientsById = /* @__PURE__ */ new Map();
356
370
  clientIdBySocket = /* @__PURE__ */ new Map();
@@ -369,9 +383,13 @@ var SecureServer = class {
369
383
  heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
370
384
  roomMembersByName = /* @__PURE__ */ new Map();
371
385
  roomNamesByClientId = /* @__PURE__ */ new Map();
386
+ clientIpByClientId = /* @__PURE__ */ new Map();
387
+ rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
388
+ rateLimitBucketsByIp = /* @__PURE__ */ new Map();
372
389
  constructor(options) {
373
- const { heartbeat, ...socketServerOptions } = options;
390
+ const { heartbeat, rateLimit, ...socketServerOptions } = options;
374
391
  this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
392
+ this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
375
393
  this.socketServer = new WebSocketServer(socketServerOptions);
376
394
  this.bindSocketServerEvents();
377
395
  this.startHeartbeatLoop();
@@ -553,6 +571,9 @@ var SecureServer = class {
553
571
  client.socket.close(code, reason);
554
572
  }
555
573
  }
574
+ this.rateLimitBucketsByClientId.clear();
575
+ this.rateLimitBucketsByIp.clear();
576
+ this.clientIpByClientId.clear();
556
577
  this.socketServer.close();
557
578
  } catch (error) {
558
579
  this.notifyError(normalizeToError(error, "Failed to close server."));
@@ -573,6 +594,211 @@ var SecureServer = class {
573
594
  timeoutMs
574
595
  };
575
596
  }
597
+ resolveRateLimitConfig(rateLimitOptions) {
598
+ const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
599
+ const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
600
+ const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
601
+ const action = rateLimitOptions?.action ?? "throttle";
602
+ const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
603
+ const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
604
+ const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
605
+ const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
606
+ const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
607
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
608
+ throw new Error("Server rateLimit windowMs must be a positive number.");
609
+ }
610
+ if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
611
+ throw new Error(
612
+ "Server rateLimit maxEventsPerConnection must be a positive number."
613
+ );
614
+ }
615
+ if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
616
+ throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
617
+ }
618
+ if (action !== "throttle" && action !== "disconnect") {
619
+ throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
620
+ }
621
+ if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
622
+ throw new Error("Server rateLimit throttleMs must be a positive number.");
623
+ }
624
+ if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
625
+ throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
626
+ }
627
+ if (maxThrottleMs < throttleMs) {
628
+ throw new Error(
629
+ "Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
630
+ );
631
+ }
632
+ if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
633
+ throw new Error(
634
+ "Server rateLimit disconnectAfterViolations must be a positive integer."
635
+ );
636
+ }
637
+ if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
638
+ throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
639
+ }
640
+ return {
641
+ enabled: rateLimitOptions?.enabled ?? true,
642
+ windowMs,
643
+ maxEventsPerConnection,
644
+ maxEventsPerIp,
645
+ action,
646
+ throttleMs,
647
+ maxThrottleMs,
648
+ disconnectAfterViolations,
649
+ disconnectCode,
650
+ disconnectReason
651
+ };
652
+ }
653
+ createRateLimitBucket(now) {
654
+ return {
655
+ windowStartedAt: now,
656
+ count: 0,
657
+ violationCount: 0,
658
+ throttleUntil: 0,
659
+ lastSeenAt: now
660
+ };
661
+ }
662
+ getOrCreateRateLimitBucket(map, key, now) {
663
+ const existingBucket = map.get(key);
664
+ if (existingBucket) {
665
+ return existingBucket;
666
+ }
667
+ const bucket = this.createRateLimitBucket(now);
668
+ map.set(key, bucket);
669
+ return bucket;
670
+ }
671
+ updateRateLimitBucket(bucket, now) {
672
+ if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
673
+ bucket.windowStartedAt = now;
674
+ bucket.count = 0;
675
+ bucket.violationCount = 0;
676
+ bucket.throttleUntil = 0;
677
+ }
678
+ bucket.count += 1;
679
+ bucket.lastSeenAt = now;
680
+ }
681
+ pruneRateLimitBucketMap(map, now, maxIdleMs) {
682
+ for (const [key, bucket] of map.entries()) {
683
+ if (now - bucket.lastSeenAt >= maxIdleMs) {
684
+ map.delete(key);
685
+ }
686
+ }
687
+ }
688
+ pruneRateLimitBuckets(now) {
689
+ const maxIdleMs = this.rateLimitConfig.windowMs * 4;
690
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
691
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
692
+ }
693
+ normalizeIpAddress(ipAddress) {
694
+ let normalized = ipAddress.trim().toLowerCase();
695
+ if (normalized.startsWith("::ffff:")) {
696
+ normalized = normalized.slice(7);
697
+ }
698
+ if (normalized.startsWith("[") && normalized.endsWith("]")) {
699
+ normalized = normalized.slice(1, -1);
700
+ }
701
+ const zoneIndex = normalized.indexOf("%");
702
+ if (zoneIndex >= 0) {
703
+ normalized = normalized.slice(0, zoneIndex);
704
+ }
705
+ return normalized.length > 0 ? normalized : "unknown";
706
+ }
707
+ resolveClientIp(request) {
708
+ const forwardedHeader = request.headers["x-forwarded-for"];
709
+ const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
710
+ if (typeof forwardedValue === "string") {
711
+ const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
712
+ if (firstForwardedIp) {
713
+ return this.normalizeIpAddress(firstForwardedIp);
714
+ }
715
+ }
716
+ return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
717
+ }
718
+ isIpStillConnected(ipAddress) {
719
+ for (const connectedIp of this.clientIpByClientId.values()) {
720
+ if (connectedIp === ipAddress) {
721
+ return true;
722
+ }
723
+ }
724
+ return false;
725
+ }
726
+ evaluateIncomingRateLimit(client) {
727
+ const noLimitDecision = {
728
+ shouldDisconnect: false,
729
+ shouldDrop: false,
730
+ throttleDelayMs: 0
731
+ };
732
+ if (!this.rateLimitConfig.enabled) {
733
+ return noLimitDecision;
734
+ }
735
+ const now = Date.now();
736
+ const clientBucket = this.getOrCreateRateLimitBucket(
737
+ this.rateLimitBucketsByClientId,
738
+ client.id,
739
+ now
740
+ );
741
+ this.updateRateLimitBucket(clientBucket, now);
742
+ const clientIp = this.clientIpByClientId.get(client.id);
743
+ const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
744
+ if (ipBucket) {
745
+ this.updateRateLimitBucket(ipBucket, now);
746
+ }
747
+ const activeThrottleUntil = Math.max(
748
+ clientBucket.throttleUntil,
749
+ ipBucket?.throttleUntil ?? 0
750
+ );
751
+ if (activeThrottleUntil > now) {
752
+ return {
753
+ shouldDisconnect: false,
754
+ shouldDrop: true,
755
+ throttleDelayMs: 0
756
+ };
757
+ }
758
+ const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
759
+ const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
760
+ if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
761
+ if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
762
+ this.pruneRateLimitBuckets(now);
763
+ }
764
+ return noLimitDecision;
765
+ }
766
+ if (isConnectionLimitExceeded) {
767
+ clientBucket.violationCount += 1;
768
+ }
769
+ if (ipBucket && isIpLimitExceeded) {
770
+ ipBucket.violationCount += 1;
771
+ }
772
+ const violationCount = Math.max(
773
+ clientBucket.violationCount,
774
+ ipBucket?.violationCount ?? 0
775
+ );
776
+ const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
777
+ if (shouldDisconnect) {
778
+ return {
779
+ shouldDisconnect: true,
780
+ shouldDrop: true,
781
+ throttleDelayMs: 0
782
+ };
783
+ }
784
+ const throttleDelayMs = Math.min(
785
+ this.rateLimitConfig.maxThrottleMs,
786
+ Math.max(
787
+ this.rateLimitConfig.throttleMs,
788
+ this.rateLimitConfig.throttleMs * violationCount
789
+ )
790
+ );
791
+ const throttleUntil = now + throttleDelayMs;
792
+ clientBucket.throttleUntil = throttleUntil;
793
+ if (ipBucket) {
794
+ ipBucket.throttleUntil = throttleUntil;
795
+ }
796
+ return {
797
+ shouldDisconnect: false,
798
+ shouldDrop: false,
799
+ throttleDelayMs
800
+ };
801
+ }
576
802
  startHeartbeatLoop() {
577
803
  if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
578
804
  return;
@@ -675,6 +901,8 @@ var SecureServer = class {
675
901
  try {
676
902
  const clientId = randomUUID();
677
903
  const handshakeState = this.createServerHandshakeState();
904
+ const clientIp = this.resolveClientIp(request);
905
+ connectionMetadata.set("network.ip", clientIp);
678
906
  const client = this.createSecureServerClient(
679
907
  clientId,
680
908
  socket,
@@ -683,6 +911,7 @@ var SecureServer = class {
683
911
  );
684
912
  this.clientsById.set(clientId, client);
685
913
  this.clientIdBySocket.set(socket, clientId);
914
+ this.clientIpByClientId.set(clientId, clientIp);
686
915
  this.handshakeStateBySocket.set(socket, handshakeState);
687
916
  this.pendingPayloadsBySocket.set(socket, []);
688
917
  this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
@@ -717,6 +946,35 @@ var SecureServer = class {
717
946
  }
718
947
  async handleIncomingMessage(client, rawData) {
719
948
  try {
949
+ const rateLimitDecision = this.evaluateIncomingRateLimit(client);
950
+ if (rateLimitDecision.shouldDisconnect) {
951
+ this.notifyError(
952
+ new Error(
953
+ `Rate limit disconnect triggered for client ${client.id}.`
954
+ )
955
+ );
956
+ if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
957
+ client.socket.close(
958
+ this.rateLimitConfig.disconnectCode,
959
+ this.rateLimitConfig.disconnectReason
960
+ );
961
+ }
962
+ return;
963
+ }
964
+ if (rateLimitDecision.shouldDrop) {
965
+ return;
966
+ }
967
+ if (rateLimitDecision.throttleDelayMs > 0) {
968
+ this.notifyError(
969
+ new Error(
970
+ `Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
971
+ )
972
+ );
973
+ await delay(rateLimitDecision.throttleDelayMs);
974
+ if (client.socket.readyState !== WebSocket.OPEN) {
975
+ return;
976
+ }
977
+ }
720
978
  let envelope = null;
721
979
  try {
722
980
  envelope = parseEnvelope(rawData);
@@ -778,6 +1036,12 @@ var SecureServer = class {
778
1036
  client.leaveAll();
779
1037
  this.clientsById.delete(client.id);
780
1038
  this.clientIdBySocket.delete(client.socket);
1039
+ const disconnectedIp = this.clientIpByClientId.get(client.id);
1040
+ this.clientIpByClientId.delete(client.id);
1041
+ this.rateLimitBucketsByClientId.delete(client.id);
1042
+ if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
1043
+ this.rateLimitBucketsByIp.delete(disconnectedIp);
1044
+ }
781
1045
  this.handshakeStateBySocket.delete(client.socket);
782
1046
  this.sharedSecretBySocket.delete(client.socket);
783
1047
  this.encryptionKeyBySocket.delete(client.socket);