@serve.zone/dcrouter 10.1.0 → 10.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "10.1.0",
4
+ "version": "10.1.2",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "exports": {
@@ -24,7 +24,7 @@
24
24
  "@git.zone/tsrun": "^2.0.1",
25
25
  "@git.zone/tstest": "^3.1.8",
26
26
  "@git.zone/tswatch": "^3.2.0",
27
- "@types/node": "^25.3.0"
27
+ "@types/node": "^25.3.3"
28
28
  },
29
29
  "dependencies": {
30
30
  "@api.global/typedrequest": "^3.2.6",
@@ -53,7 +53,7 @@
53
53
  "@push.rocks/smartradius": "^1.1.1",
54
54
  "@push.rocks/smartrequest": "^5.0.1",
55
55
  "@push.rocks/smartrx": "^3.0.10",
56
- "@push.rocks/smartstate": "^2.0.30",
56
+ "@push.rocks/smartstate": "^2.1.1",
57
57
  "@push.rocks/smartunique": "^3.0.9",
58
58
  "@serve.zone/catalog": "^2.5.0",
59
59
  "@serve.zone/interfaces": "^5.3.0",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '10.1.0',
6
+ version: '10.1.2',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -23,6 +23,7 @@ import { MetricsManager } from './monitoring/index.js';
23
23
  import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
24
24
  import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
25
25
  import { RouteConfigManager, ApiTokenManager } from './config/index.js';
26
+ import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
26
27
 
27
28
  export interface IDcRouterOptions {
28
29
  /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -956,6 +957,7 @@ export class DcRouter {
956
957
  // Stop cache database after other services (they may need it during shutdown)
957
958
  if (this.cacheDb) {
958
959
  await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
960
+ CacheDb.resetInstance();
959
961
  }
960
962
 
961
963
  // Clear backoff cache in cert scheduler
@@ -979,6 +981,11 @@ export class DcRouter {
979
981
  this.apiTokenManager = undefined;
980
982
  this.certificateStatusMap.clear();
981
983
 
984
+ // Reset security singletons to allow GC
985
+ SecurityLogger.resetInstance();
986
+ ContentScanner.resetInstance();
987
+ IPReputationChecker.resetInstance();
988
+
982
989
  logger.log('info', 'All DcRouter services stopped');
983
990
  } catch (error) {
984
991
  logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
@@ -1363,15 +1370,25 @@ export class DcRouter {
1363
1370
  return;
1364
1371
  }
1365
1372
 
1373
+ // Prevent uncaught exception from socket 'error' events
1374
+ socket.on('error', (err) => {
1375
+ logger.log('error', `DNS socket error: ${err.message}`);
1376
+ if (!socket.destroyed) {
1377
+ socket.destroy();
1378
+ }
1379
+ });
1380
+
1366
1381
  logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
1367
-
1382
+
1368
1383
  try {
1369
1384
  // Use the built-in socket handler from smartdns
1370
1385
  // This handles HTTP/2, DoH protocol, etc.
1371
1386
  await (this.dnsServer as any).handleHttpsSocket(socket);
1372
1387
  } catch (error) {
1373
1388
  logger.log('error', `DNS socket handler error: ${error.message}`);
1374
- socket.destroy();
1389
+ if (!socket.destroyed) {
1390
+ socket.destroy();
1391
+ }
1375
1392
  }
1376
1393
  };
1377
1394
  }
@@ -111,6 +111,15 @@ export class MetricsManager {
111
111
  this.securityMetrics.lastResetDate = currentDate;
112
112
  }
113
113
 
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
+
114
123
  // Prune old time-series buckets every minute (don't wait for lazy query)
115
124
  this.pruneOldBuckets();
116
125
  }, 60000); // Check every minute
@@ -427,13 +436,9 @@ export class MetricsManager {
427
436
  this.dnsMetrics.cacheMisses++;
428
437
  }
429
438
 
430
- // Track query timestamp
439
+ // Track query timestamp (pruning moved to resetInterval to avoid O(n) per query)
431
440
  this.dnsMetrics.queryTimestamps.push(Date.now());
432
441
 
433
- // Keep only timestamps from last 5 minutes
434
- const fiveMinutesAgo = Date.now() - 300000;
435
- this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
436
-
437
442
  // Track response time if provided
438
443
  if (responseTimeMs) {
439
444
  this.dnsMetrics.responseTimes.push(responseTimeMs);
@@ -318,11 +318,15 @@ export class LogsHandler {
318
318
  try {
319
319
  // Use a timeout to detect hung streams (sendData can hang if the
320
320
  // VirtualStream's keepAlive loop has ended)
321
+ let timeoutHandle: ReturnType<typeof setTimeout>;
321
322
  await Promise.race([
322
- virtualStream.sendData(encoder.encode(logData)),
323
- new Promise<never>((_, reject) =>
324
- setTimeout(() => reject(new Error('stream send timeout')), 10_000)
325
- ),
323
+ virtualStream.sendData(encoder.encode(logData)).then((result) => {
324
+ clearTimeout(timeoutHandle);
325
+ return result;
326
+ }),
327
+ new Promise<never>((_, reject) => {
328
+ timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
329
+ }),
326
330
  ]);
327
331
  } catch {
328
332
  // Stream closed, errored, or timed out — clean up
@@ -182,7 +182,14 @@ export class ContentScanner {
182
182
  }
183
183
  return ContentScanner.instance;
184
184
  }
185
-
185
+
186
+ /**
187
+ * Reset the singleton instance (for shutdown/testing)
188
+ */
189
+ public static resetInstance(): void {
190
+ ContentScanner.instance = undefined;
191
+ }
192
+
186
193
  /**
187
194
  * Scan an email for malicious content
188
195
  * @param email The email to scan
@@ -65,6 +65,8 @@ export class IPReputationChecker {
65
65
  private reputationCache: LRUCache<string, IReputationResult>;
66
66
  private options: Required<IIPReputationOptions>;
67
67
  private storageManager?: any; // StorageManager instance
68
+ private saveCacheTimer: ReturnType<typeof setTimeout> | null = null;
69
+ private static readonly SAVE_CACHE_DEBOUNCE_MS = 30_000;
68
70
 
69
71
  // Default DNSBL servers
70
72
  private static readonly DEFAULT_DNSBL_SERVERS = [
@@ -143,7 +145,20 @@ export class IPReputationChecker {
143
145
  }
144
146
  return IPReputationChecker.instance;
145
147
  }
146
-
148
+
149
+ /**
150
+ * Reset the singleton instance (for shutdown/testing)
151
+ */
152
+ public static resetInstance(): void {
153
+ if (IPReputationChecker.instance) {
154
+ if (IPReputationChecker.instance.saveCacheTimer) {
155
+ clearTimeout(IPReputationChecker.instance.saveCacheTimer);
156
+ IPReputationChecker.instance.saveCacheTimer = null;
157
+ }
158
+ }
159
+ IPReputationChecker.instance = undefined;
160
+ }
161
+
147
162
  /**
148
163
  * Check an IP address's reputation
149
164
  * @param ip IP address to check
@@ -213,12 +228,9 @@ export class IPReputationChecker {
213
228
  // Update cache with result
214
229
  this.reputationCache.set(ip, result);
215
230
 
216
- // Save cache if enabled
231
+ // Schedule debounced cache save if enabled
217
232
  if (this.options.enableLocalCache) {
218
- // Fire and forget the save operation
219
- this.saveCache().catch(error => {
220
- logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
221
- });
233
+ this.debouncedSaveCache();
222
234
  }
223
235
 
224
236
  // Log the reputation check
@@ -447,6 +459,21 @@ export class IPReputationChecker {
447
459
  });
448
460
  }
449
461
 
462
+ /**
463
+ * Schedule a debounced cache save (at most once per SAVE_CACHE_DEBOUNCE_MS)
464
+ */
465
+ private debouncedSaveCache(): void {
466
+ if (this.saveCacheTimer) {
467
+ return; // already scheduled
468
+ }
469
+ this.saveCacheTimer = setTimeout(() => {
470
+ this.saveCacheTimer = null;
471
+ this.saveCache().catch(error => {
472
+ logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
473
+ });
474
+ }, IPReputationChecker.SAVE_CACHE_DEBOUNCE_MS);
475
+ }
476
+
450
477
  /**
451
478
  * Save cache to disk or storage manager
452
479
  */
@@ -83,7 +83,14 @@ export class SecurityLogger {
83
83
  }
84
84
  return SecurityLogger.instance;
85
85
  }
86
-
86
+
87
+ /**
88
+ * Reset the singleton instance (for shutdown/testing)
89
+ */
90
+ public static resetInstance(): void {
91
+ SecurityLogger.instance = undefined;
92
+ }
93
+
87
94
  /**
88
95
  * Log a security event
89
96
  * @param event The security event to log
@@ -30,6 +30,7 @@ export type StorageBackend = 'filesystem' | 'custom' | 'memory';
30
30
  * Provides unified key-value storage with multiple backend support
31
31
  */
32
32
  export class StorageManager {
33
+ private static readonly MAX_MEMORY_ENTRIES = 10_000;
33
34
  private backend: StorageBackend;
34
35
  private memoryStore: Map<string, string> = new Map();
35
36
  private config: IStorageConfig;
@@ -227,6 +228,11 @@ export class StorageManager {
227
228
 
228
229
  case 'memory': {
229
230
  this.memoryStore.set(key, value);
231
+ // Evict oldest entries if memory store exceeds limit
232
+ while (this.memoryStore.size > StorageManager.MAX_MEMORY_ENTRIES) {
233
+ const firstKey = this.memoryStore.keys().next().value;
234
+ this.memoryStore.delete(firstKey);
235
+ }
230
236
  break;
231
237
  }
232
238
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '10.1.0',
6
+ version: '10.1.2',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -154,7 +154,7 @@ export class OpsViewApiTokens extends DeesElement {
154
154
  },
155
155
  {
156
156
  name: 'Roll',
157
- iconName: 'lucide:refresh-cw',
157
+ iconName: 'lucide:rotate-cw',
158
158
  type: ['inRow', 'contextmenu'] as any,
159
159
  actionFunc: async (actionData: any) => {
160
160
  const token = actionData.item as interfaces.data.IApiTokenInfo;
@@ -306,7 +306,7 @@ export class OpsViewApiTokens extends DeesElement {
306
306
  },
307
307
  {
308
308
  name: 'Roll Token',
309
- iconName: 'lucide:refresh-cw',
309
+ iconName: 'lucide:rotate-cw',
310
310
  action: async (modalArg: any) => {
311
311
  await modalArg.destroy();
312
312
  try {
@@ -76,8 +76,15 @@ export class OpsViewLogs extends DeesElement {
76
76
  // Wait for xterm terminal to finish initializing (CDN load)
77
77
  if (!chartLog.terminalReady) {
78
78
  await new Promise<void>((resolve) => {
79
+ let attempts = 0;
80
+ const maxAttempts = 200; // 200 * 50ms = 10 seconds
79
81
  const check = () => {
80
82
  if (chartLog.terminalReady) { resolve(); return; }
83
+ if (++attempts >= maxAttempts) {
84
+ console.warn('ops-view-logs: terminal ready timeout after 10s');
85
+ resolve(); // resolve gracefully to avoid blocking
86
+ return;
87
+ }
81
88
  setTimeout(check, 50);
82
89
  };
83
90
  check();