@serve.zone/dcrouter 10.1.3 → 10.1.6

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.
@@ -35,7 +35,9 @@ export class MetricsManager {
35
35
  queryTypes: {} as Record<string, number>,
36
36
  topDomains: new Map<string, number>(),
37
37
  lastResetDate: new Date().toDateString(),
38
- queryTimestamps: [] as number[], // Track query timestamps for rate calculation
38
+ // Per-second query count ring buffer (300 entries = 5 minutes)
39
+ queryRing: new Int32Array(300),
40
+ queryRingLastSecond: 0, // last epoch second that was written
39
41
  responseTimes: [] as number[], // Track response times in ms
40
42
  recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
41
43
  };
@@ -95,12 +97,13 @@ export class MetricsManager {
95
97
  this.dnsMetrics.cacheMisses = 0;
96
98
  this.dnsMetrics.queryTypes = {};
97
99
  this.dnsMetrics.topDomains.clear();
98
- this.dnsMetrics.queryTimestamps = [];
100
+ this.dnsMetrics.queryRing.fill(0);
101
+ this.dnsMetrics.queryRingLastSecond = 0;
99
102
  this.dnsMetrics.responseTimes = [];
100
103
  this.dnsMetrics.recentQueries = [];
101
104
  this.dnsMetrics.lastResetDate = currentDate;
102
105
  }
103
-
106
+
104
107
  if (currentDate !== this.securityMetrics.lastResetDate) {
105
108
  this.securityMetrics.blockedIPs = 0;
106
109
  this.securityMetrics.authFailures = 0;
@@ -111,15 +114,6 @@ export class MetricsManager {
111
114
  this.securityMetrics.lastResetDate = currentDate;
112
115
  }
113
116
 
114
- // Prune old query timestamps (keep last 5 minutes)
115
- const fiveMinutesAgo = Date.now() - 300000;
116
- const idx = this.dnsMetrics.queryTimestamps.findIndex(ts => ts >= fiveMinutesAgo);
117
- if (idx > 0) {
118
- this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.slice(idx);
119
- } else if (idx === -1) {
120
- this.dnsMetrics.queryTimestamps = [];
121
- }
122
-
123
117
  // Prune old time-series buckets every minute (don't wait for lazy query)
124
118
  this.pruneOldBuckets();
125
119
  }, 60000); // Check every minute
@@ -150,16 +144,16 @@ export class MetricsManager {
150
144
  const smartMetricsData = await this.smartMetrics.getMetrics();
151
145
  const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
152
146
  const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
147
+ const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
153
148
 
154
149
  return {
155
150
  uptime: process.uptime(),
156
151
  startTime: Date.now() - (process.uptime() * 1000),
157
152
  memoryUsage: {
158
- heapUsed: process.memoryUsage().heapUsed,
159
- heapTotal: process.memoryUsage().heapTotal,
160
- external: process.memoryUsage().external,
161
- rss: process.memoryUsage().rss,
162
- // Add SmartMetrics memory data
153
+ heapUsed,
154
+ heapTotal,
155
+ external,
156
+ rss,
163
157
  maxMemoryMB: this.smartMetrics.maxMemoryMB,
164
158
  actualUsageBytes: smartMetricsData.memoryUsageBytes,
165
159
  actualUsagePercentage: smartMetricsData.memoryPercentage,
@@ -228,11 +222,8 @@ export class MetricsManager {
228
222
  .slice(0, 10)
229
223
  .map(([domain, count]) => ({ domain, count }));
230
224
 
231
- // Calculate queries per second from recent timestamps
232
- const now = Date.now();
233
- const oneMinuteAgo = now - 60000;
234
- const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
235
- const queriesPerSecond = recentQueries.length / 60;
225
+ // Calculate queries per second from ring buffer (sum last 60 seconds)
226
+ const queriesPerSecond = this.getQueryRingSum(60) / 60;
236
227
 
237
228
  // Calculate average response time
238
229
  const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
@@ -436,8 +427,8 @@ export class MetricsManager {
436
427
  this.dnsMetrics.cacheMisses++;
437
428
  }
438
429
 
439
- // Track query timestamp (pruning moved to resetInterval to avoid O(n) per query)
440
- this.dnsMetrics.queryTimestamps.push(Date.now());
430
+ // Increment per-second query counter in ring buffer
431
+ this.incrementQueryRing();
441
432
 
442
433
  // Track response time if provided
443
434
  if (responseTimeMs) {
@@ -609,7 +600,7 @@ export class MetricsManager {
609
600
  requestsPerSecond,
610
601
  requestsTotal,
611
602
  };
612
- }, 200); // Use 200ms cache for more frequent updates
603
+ }, 1000); // 1s cache matches typical dashboard poll interval
613
604
  }
614
605
 
615
606
  // --- Time-series helpers ---
@@ -638,6 +629,63 @@ export class MetricsManager {
638
629
  bucket.queries++;
639
630
  }
640
631
 
632
+ /**
633
+ * Increment the per-second query counter in the ring buffer.
634
+ * Zeros any stale slots between the last write and the current second.
635
+ */
636
+ private incrementQueryRing(): void {
637
+ const currentSecond = Math.floor(Date.now() / 1000);
638
+ const ring = this.dnsMetrics.queryRing;
639
+ const last = this.dnsMetrics.queryRingLastSecond;
640
+
641
+ if (last === 0) {
642
+ // First call — zero and anchor
643
+ ring.fill(0);
644
+ this.dnsMetrics.queryRingLastSecond = currentSecond;
645
+ ring[currentSecond % ring.length] = 1;
646
+ return;
647
+ }
648
+
649
+ const gap = currentSecond - last;
650
+ if (gap >= ring.length) {
651
+ // Entire ring is stale — clear all
652
+ ring.fill(0);
653
+ } else if (gap > 0) {
654
+ // Zero slots from (last+1) to currentSecond (inclusive)
655
+ for (let s = last + 1; s <= currentSecond; s++) {
656
+ ring[s % ring.length] = 0;
657
+ }
658
+ }
659
+
660
+ this.dnsMetrics.queryRingLastSecond = currentSecond;
661
+ ring[currentSecond % ring.length]++;
662
+ }
663
+
664
+ /**
665
+ * Sum query counts from the ring buffer for the last N seconds.
666
+ */
667
+ private getQueryRingSum(seconds: number): number {
668
+ const currentSecond = Math.floor(Date.now() / 1000);
669
+ const ring = this.dnsMetrics.queryRing;
670
+ const last = this.dnsMetrics.queryRingLastSecond;
671
+
672
+ if (last === 0) return 0;
673
+
674
+ // First, zero stale slots so reads are accurate even without writes
675
+ const gap = currentSecond - last;
676
+ if (gap >= ring.length) return 0; // all data is stale
677
+
678
+ let sum = 0;
679
+ const limit = Math.min(seconds, ring.length);
680
+ for (let i = 0; i < limit; i++) {
681
+ const sec = currentSecond - i;
682
+ if (sec < last - (ring.length - 1)) break; // slot is from older cycle
683
+ if (sec > last) continue; // no writes yet for this second
684
+ sum += ring[sec % ring.length];
685
+ }
686
+ return sum;
687
+ }
688
+
641
689
  private pruneOldBuckets(): void {
642
690
  const cutoff = Date.now() - 86400000; // 24h
643
691
  for (const key of this.emailMinuteBuckets.keys()) {
@@ -162,8 +162,9 @@ export class SecurityLogger {
162
162
  }
163
163
  }
164
164
 
165
- // Return most recent events up to limit
165
+ // Return most recent events up to limit (slice first to avoid mutating source)
166
166
  return filteredEvents
167
+ .slice()
167
168
  .sort((a, b) => b.timestamp - a.timestamp)
168
169
  .slice(0, limit);
169
170
  }
@@ -249,58 +250,46 @@ export class SecurityLogger {
249
250
  topIPs: Array<{ ip: string; count: number }>;
250
251
  topDomains: Array<{ domain: string; count: number }>;
251
252
  } {
252
- // Filter by time window if provided
253
- let events = this.securityEvents;
254
- if (timeWindow) {
255
- const cutoff = Date.now() - timeWindow;
256
- events = events.filter(e => e.timestamp >= cutoff);
253
+ const cutoff = timeWindow ? Date.now() - timeWindow : 0;
254
+
255
+ // Initialize counters
256
+ const byLevel = {} as Record<SecurityLogLevel, number>;
257
+ for (const level of Object.values(SecurityLogLevel)) {
258
+ byLevel[level] = 0;
259
+ }
260
+ const byType = {} as Record<SecurityEventType, number>;
261
+ for (const type of Object.values(SecurityEventType)) {
262
+ byType[type] = 0;
257
263
  }
258
-
259
- // Count by level
260
- const byLevel = Object.values(SecurityLogLevel).reduce((acc, level) => {
261
- acc[level] = events.filter(e => e.level === level).length;
262
- return acc;
263
- }, {} as Record<SecurityLogLevel, number>);
264
-
265
- // Count by type
266
- const byType = Object.values(SecurityEventType).reduce((acc, type) => {
267
- acc[type] = events.filter(e => e.type === type).length;
268
- return acc;
269
- }, {} as Record<SecurityEventType, number>);
270
-
271
- // Count by IP
272
264
  const ipCounts = new Map<string, number>();
273
- events.forEach(e => {
265
+ const domainCounts = new Map<string, number>();
266
+
267
+ // Single pass over all events
268
+ let total = 0;
269
+ for (const e of this.securityEvents) {
270
+ if (cutoff && e.timestamp < cutoff) continue;
271
+ total++;
272
+ byLevel[e.level]++;
273
+ byType[e.type]++;
274
274
  if (e.ipAddress) {
275
275
  ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1);
276
276
  }
277
- });
278
-
279
- // Count by domain
280
- const domainCounts = new Map<string, number>();
281
- events.forEach(e => {
282
277
  if (e.domain) {
283
278
  domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
284
279
  }
285
- });
286
-
280
+ }
281
+
287
282
  // Sort and limit top entries
288
283
  const topIPs = Array.from(ipCounts.entries())
289
284
  .map(([ip, count]) => ({ ip, count }))
290
285
  .sort((a, b) => b.count - a.count)
291
286
  .slice(0, 10);
292
-
287
+
293
288
  const topDomains = Array.from(domainCounts.entries())
294
289
  .map(([domain, count]) => ({ domain, count }))
295
290
  .sort((a, b) => b.count - a.count)
296
291
  .slice(0, 10);
297
-
298
- return {
299
- total: events.length,
300
- byLevel,
301
- byType,
302
- topIPs,
303
- topDomains
304
- };
292
+
293
+ return { total, byLevel, byType, topIPs, topDomains };
305
294
  }
306
295
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '10.1.3',
6
+ version: '10.1.6',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -581,7 +581,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
581
581
  });
582
582
 
583
583
  export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
584
- async (statePartArg, domain) => {
584
+ async (statePartArg, domain, actionContext) => {
585
585
  const context = getActionContext();
586
586
  const currentState = statePartArg.getState();
587
587
 
@@ -596,8 +596,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
596
596
  });
597
597
 
598
598
  // Re-fetch overview after reprovisioning
599
- await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
600
- return statePartArg.getState();
599
+ return await actionContext.dispatch(fetchCertificateOverviewAction, null);
601
600
  } catch (error) {
602
601
  return {
603
602
  ...currentState,
@@ -608,7 +607,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
608
607
  );
609
608
 
610
609
  export const deleteCertificateAction = certificateStatePart.createAction<string>(
611
- async (statePartArg, domain) => {
610
+ async (statePartArg, domain, actionContext) => {
612
611
  const context = getActionContext();
613
612
  const currentState = statePartArg.getState();
614
613
 
@@ -623,8 +622,7 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
623
622
  });
624
623
 
625
624
  // Re-fetch overview after deletion
626
- await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
627
- return statePartArg.getState();
625
+ return await actionContext.dispatch(fetchCertificateOverviewAction, null);
628
626
  } catch (error) {
629
627
  return {
630
628
  ...currentState,
@@ -643,7 +641,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
643
641
  publicKey: string;
644
642
  csr: string;
645
643
  }>(
646
- async (statePartArg, cert) => {
644
+ async (statePartArg, cert, actionContext) => {
647
645
  const context = getActionContext();
648
646
  const currentState = statePartArg.getState();
649
647
 
@@ -658,8 +656,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
658
656
  });
659
657
 
660
658
  // Re-fetch overview after import
661
- await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
662
- return statePartArg.getState();
659
+ return await actionContext.dispatch(fetchCertificateOverviewAction, null);
663
660
  } catch (error) {
664
661
  return {
665
662
  ...currentState,
@@ -737,7 +734,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
737
734
  listenPorts?: number[];
738
735
  autoDerivePorts?: boolean;
739
736
  tags?: string[];
740
- }>(async (statePartArg, dataArg) => {
737
+ }>(async (statePartArg, dataArg, actionContext) => {
741
738
  const context = getActionContext();
742
739
  const currentState = statePartArg.getState();
743
740
 
@@ -756,7 +753,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
756
753
 
757
754
  if (response.success) {
758
755
  // Refresh the list
759
- await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
756
+ await actionContext.dispatch(fetchRemoteIngressAction, null);
760
757
 
761
758
  return {
762
759
  ...statePartArg.getState(),
@@ -774,7 +771,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
774
771
  });
775
772
 
776
773
  export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
777
- async (statePartArg, edgeId) => {
774
+ async (statePartArg, edgeId, actionContext) => {
778
775
  const context = getActionContext();
779
776
  const currentState = statePartArg.getState();
780
777
 
@@ -788,8 +785,7 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
788
785
  id: edgeId,
789
786
  });
790
787
 
791
- await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
792
- return statePartArg.getState();
788
+ return await actionContext.dispatch(fetchRemoteIngressAction, null);
793
789
  } catch (error) {
794
790
  return {
795
791
  ...currentState,
@@ -805,7 +801,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
805
801
  listenPorts?: number[];
806
802
  autoDerivePorts?: boolean;
807
803
  tags?: string[];
808
- }>(async (statePartArg, dataArg) => {
804
+ }>(async (statePartArg, dataArg, actionContext) => {
809
805
  const context = getActionContext();
810
806
  const currentState = statePartArg.getState();
811
807
 
@@ -823,8 +819,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
823
819
  tags: dataArg.tags,
824
820
  });
825
821
 
826
- await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
827
- return statePartArg.getState();
822
+ return await actionContext.dispatch(fetchRemoteIngressAction, null);
828
823
  } catch (error) {
829
824
  return {
830
825
  ...currentState,
@@ -877,7 +872,7 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
877
872
  export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
878
873
  id: string;
879
874
  enabled: boolean;
880
- }>(async (statePartArg, dataArg) => {
875
+ }>(async (statePartArg, dataArg, actionContext) => {
881
876
  const context = getActionContext();
882
877
  const currentState = statePartArg.getState();
883
878
 
@@ -892,8 +887,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
892
887
  enabled: dataArg.enabled,
893
888
  });
894
889
 
895
- await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
896
- return statePartArg.getState();
890
+ return await actionContext.dispatch(fetchRemoteIngressAction, null);
897
891
  } catch (error) {
898
892
  return {
899
893
  ...currentState,
@@ -939,7 +933,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
939
933
  export const createRouteAction = routeManagementStatePart.createAction<{
940
934
  route: any;
941
935
  enabled?: boolean;
942
- }>(async (statePartArg, dataArg) => {
936
+ }>(async (statePartArg, dataArg, actionContext) => {
943
937
  const context = getActionContext();
944
938
  const currentState = statePartArg.getState();
945
939
 
@@ -954,8 +948,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
954
948
  enabled: dataArg.enabled,
955
949
  });
956
950
 
957
- await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
958
- return statePartArg.getState();
951
+ return await actionContext.dispatch(fetchMergedRoutesAction, null);
959
952
  } catch (error) {
960
953
  return {
961
954
  ...currentState,
@@ -965,7 +958,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
965
958
  });
966
959
 
967
960
  export const deleteRouteAction = routeManagementStatePart.createAction<string>(
968
- async (statePartArg, routeId) => {
961
+ async (statePartArg, routeId, actionContext) => {
969
962
  const context = getActionContext();
970
963
  const currentState = statePartArg.getState();
971
964
 
@@ -979,8 +972,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
979
972
  id: routeId,
980
973
  });
981
974
 
982
- await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
983
- return statePartArg.getState();
975
+ return await actionContext.dispatch(fetchMergedRoutesAction, null);
984
976
  } catch (error) {
985
977
  return {
986
978
  ...currentState,
@@ -993,7 +985,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
993
985
  export const toggleRouteAction = routeManagementStatePart.createAction<{
994
986
  id: string;
995
987
  enabled: boolean;
996
- }>(async (statePartArg, dataArg) => {
988
+ }>(async (statePartArg, dataArg, actionContext) => {
997
989
  const context = getActionContext();
998
990
  const currentState = statePartArg.getState();
999
991
 
@@ -1008,8 +1000,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
1008
1000
  enabled: dataArg.enabled,
1009
1001
  });
1010
1002
 
1011
- await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1012
- return statePartArg.getState();
1003
+ return await actionContext.dispatch(fetchMergedRoutesAction, null);
1013
1004
  } catch (error) {
1014
1005
  return {
1015
1006
  ...currentState,
@@ -1021,7 +1012,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
1021
1012
  export const setRouteOverrideAction = routeManagementStatePart.createAction<{
1022
1013
  routeName: string;
1023
1014
  enabled: boolean;
1024
- }>(async (statePartArg, dataArg) => {
1015
+ }>(async (statePartArg, dataArg, actionContext) => {
1025
1016
  const context = getActionContext();
1026
1017
  const currentState = statePartArg.getState();
1027
1018
 
@@ -1036,8 +1027,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
1036
1027
  enabled: dataArg.enabled,
1037
1028
  });
1038
1029
 
1039
- await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1040
- return statePartArg.getState();
1030
+ return await actionContext.dispatch(fetchMergedRoutesAction, null);
1041
1031
  } catch (error) {
1042
1032
  return {
1043
1033
  ...currentState,
@@ -1047,7 +1037,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
1047
1037
  });
1048
1038
 
1049
1039
  export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
1050
- async (statePartArg, routeName) => {
1040
+ async (statePartArg, routeName, actionContext) => {
1051
1041
  const context = getActionContext();
1052
1042
  const currentState = statePartArg.getState();
1053
1043
 
@@ -1061,8 +1051,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
1061
1051
  routeName,
1062
1052
  });
1063
1053
 
1064
- await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1065
- return statePartArg.getState();
1054
+ return await actionContext.dispatch(fetchMergedRoutesAction, null);
1066
1055
  } catch (error) {
1067
1056
  return {
1068
1057
  ...currentState,
@@ -1128,7 +1117,7 @@ export async function rollApiToken(id: string) {
1128
1117
  }
1129
1118
 
1130
1119
  export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
1131
- async (statePartArg, tokenId) => {
1120
+ async (statePartArg, tokenId, actionContext) => {
1132
1121
  const context = getActionContext();
1133
1122
  const currentState = statePartArg.getState();
1134
1123
 
@@ -1142,8 +1131,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
1142
1131
  id: tokenId,
1143
1132
  });
1144
1133
 
1145
- await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
1146
- return statePartArg.getState();
1134
+ return await actionContext.dispatch(fetchApiTokensAction, null);
1147
1135
  } catch (error) {
1148
1136
  return {
1149
1137
  ...currentState,
@@ -1156,7 +1144,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
1156
1144
  export const toggleApiTokenAction = routeManagementStatePart.createAction<{
1157
1145
  id: string;
1158
1146
  enabled: boolean;
1159
- }>(async (statePartArg, dataArg) => {
1147
+ }>(async (statePartArg, dataArg, actionContext) => {
1160
1148
  const context = getActionContext();
1161
1149
  const currentState = statePartArg.getState();
1162
1150
 
@@ -1171,8 +1159,7 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
1171
1159
  enabled: dataArg.enabled,
1172
1160
  });
1173
1161
 
1174
- await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
1175
- return statePartArg.getState();
1162
+ return await actionContext.dispatch(fetchApiTokensAction, null);
1176
1163
  } catch (error) {
1177
1164
  return {
1178
1165
  ...currentState,