@serve.zone/dcrouter 13.32.0 → 13.33.0

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": "13.32.0",
4
+ "version": "13.33.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "exports": {
@@ -34,7 +34,7 @@
34
34
  "@push.rocks/qenv": "^6.1.4",
35
35
  "@push.rocks/smartacme": "^9.5.0",
36
36
  "@push.rocks/smartdata": "^7.1.7",
37
- "@push.rocks/smartdb": "^2.10.0",
37
+ "@push.rocks/smartdb": "^2.10.1",
38
38
  "@push.rocks/smartdns": "^7.9.2",
39
39
  "@push.rocks/smartfs": "^1.5.1",
40
40
  "@push.rocks/smartguard": "^3.1.0",
@@ -43,10 +43,10 @@
43
43
  "@push.rocks/smartmetrics": "^3.0.3",
44
44
  "@push.rocks/smartmigration": "1.4.1",
45
45
  "@push.rocks/smartmta": "^5.3.3",
46
- "@push.rocks/smartnetwork": "^4.7.1",
46
+ "@push.rocks/smartnetwork": "^4.7.2",
47
47
  "@push.rocks/smartpath": "^6.0.0",
48
48
  "@push.rocks/smartpromise": "^4.2.4",
49
- "@push.rocks/smartproxy": "^27.10.2",
49
+ "@push.rocks/smartproxy": "^27.10.3",
50
50
  "@push.rocks/smartradius": "^1.1.2",
51
51
  "@push.rocks/smartrequest": "^5.0.3",
52
52
  "@push.rocks/smartrx": "^3.0.10",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.32.0',
6
+ version: '13.33.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -608,9 +608,23 @@ export class RouteConfigManager {
608
608
  routeId?: string,
609
609
  ): plugins.smartproxy.IRouteConfig {
610
610
  const dcRoute = route as IDcRouterRouteConfig;
611
- if (!dcRoute.vpnOnly) return route;
612
-
613
611
  const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
612
+
613
+ if (!dcRoute.vpnOnly) {
614
+ const existingAllowList = route.security?.ipAllowList;
615
+ if (!Array.isArray(existingAllowList) || existingAllowList.length === 0 || vpnEntries.length === 0) {
616
+ return route;
617
+ }
618
+
619
+ return {
620
+ ...route,
621
+ security: {
622
+ ...route.security,
623
+ ipAllowList: this.mergeIpAllowEntries(existingAllowList as TIpAllowEntry[], vpnEntries),
624
+ },
625
+ };
626
+ }
627
+
614
628
  const existingBlockList = route.security?.ipBlockList || [];
615
629
  const ipBlockList = vpnEntries.length
616
630
  ? existingBlockList
@@ -625,4 +639,23 @@ export class RouteConfigManager {
625
639
  },
626
640
  };
627
641
  }
642
+
643
+ private mergeIpAllowEntries(
644
+ existingEntries: TIpAllowEntry[],
645
+ vpnEntries: TIpAllowEntry[],
646
+ ): TIpAllowEntry[] {
647
+ const merged: TIpAllowEntry[] = [];
648
+ const seen = new Set<string>();
649
+
650
+ for (const entry of [...existingEntries, ...vpnEntries]) {
651
+ const key = typeof entry === 'string'
652
+ ? `ip:${entry}`
653
+ : `domain:${entry.ip}:${[...entry.domains].sort().join(',')}`;
654
+ if (seen.has(key)) continue;
655
+ seen.add(key);
656
+ merged.push(entry);
657
+ }
658
+
659
+ return merged;
660
+ }
628
661
  }
@@ -217,7 +217,7 @@ export class TargetProfileManager {
217
217
  allRoutes: Map<string, IRoute> = new Map(),
218
218
  ): Array<string | { ip: string; domains: string[] }> {
219
219
  const entries: Array<string | { ip: string; domains: string[] }> = [];
220
- const routeDomains: string[] = (route.match as any)?.domains || [];
220
+ const routeDomains = this.getRouteDomains(route);
221
221
  const routeNameIndex = this.buildRouteNameIndex(allRoutes);
222
222
 
223
223
  for (const client of clients) {
@@ -298,11 +298,8 @@ export class TargetProfileManager {
298
298
  profile,
299
299
  routeNameIndex,
300
300
  )) {
301
- const routeDomains = (route.route.match as any)?.domains;
302
- if (Array.isArray(routeDomains)) {
303
- for (const d of routeDomains) {
304
- domains.add(d);
305
- }
301
+ for (const d of this.getRouteDomains(route.route as IDcRouterRouteConfig)) {
302
+ domains.add(d);
306
303
  }
307
304
  }
308
305
  }
@@ -327,7 +324,7 @@ export class TargetProfileManager {
327
324
  profile: ITargetProfile,
328
325
  routeNameIndex: Map<string, string[]>,
329
326
  ): boolean {
330
- const routeDomains: string[] = (route.match as any)?.domains || [];
327
+ const routeDomains = this.getRouteDomains(route);
331
328
  const result = this.routeMatchesProfileDetailed(
332
329
  route,
333
330
  routeId,
@@ -425,6 +422,12 @@ export class TargetProfileManager {
425
422
  return false;
426
423
  }
427
424
 
425
+ private getRouteDomains(route: IDcRouterRouteConfig): string[] {
426
+ const domains = (route.match as any)?.domains;
427
+ if (!domains) return [];
428
+ return Array.isArray(domains) ? domains : [domains];
429
+ }
430
+
428
431
  private normalizeRouteRefs(routeRefs?: string[]): string[] | undefined {
429
432
  const allRoutes = this.getAllRoutes?.() || new Map<string, IRoute>();
430
433
  return this.normalizeRouteRefsAgainstRoutes(routeRefs, allRoutes, 'strict');
@@ -725,7 +725,10 @@ export class MetricsManager {
725
725
  .slice(0, 10)
726
726
  .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
727
727
 
728
- void this.dcRouter.securityPolicyManager?.observeIps([...allIPData.keys()]);
728
+ this.dcRouter.securityPolicyManager?.queueObservedIps([
729
+ ...topIPs.map((item) => item.ip),
730
+ ...topIPsByBandwidth.map((item) => item.ip),
731
+ ]);
729
732
 
730
733
  // Build domain activity using per-IP domain request counts from Rust engine
731
734
  const connectionsByRoute = proxyMetrics.connections.byRoute();
@@ -24,7 +24,8 @@ export class AdminHandler {
24
24
  // JWT instance
25
25
  public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
26
26
 
27
- // Ephemeral bootstrap users. Persisted accounts take over once an active admin exists.
27
+ // Ephemeral bootstrap users. DB-backed instances may use these only until the
28
+ // database is ready and the first persistent admin account has been created.
28
29
  private users = new Map<string, {
29
30
  id: string;
30
31
  username: string;
@@ -87,9 +88,12 @@ export class AdminHandler {
87
88
  * Used by UsersHandler to serve the admin-only listUsers endpoint.
88
89
  */
89
90
  public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
90
- if (await this.hasPersistentAdminAccount()) {
91
- const store = this.getAccountStore();
92
- const accounts = await store!.listAccounts();
91
+ const accountState = await this.getPersistentAccountState();
92
+ if (accountState.dbEnabled && !accountState.dbReady) {
93
+ throw new plugins.typedrequest.TypedResponseError('database is not ready');
94
+ }
95
+ if (accountState.hasPersistentAdmin) {
96
+ const accounts = await accountState.store!.listAccounts();
93
97
  return accounts.map((accountArg) => this.accountToUser(accountArg));
94
98
  }
95
99
 
@@ -101,16 +105,14 @@ export class AdminHandler {
101
105
  }
102
106
 
103
107
  public async getBootstrapStatus(): Promise<interfaces.requests.IReq_GetAdminBootstrapStatus['response']> {
104
- const dbEnabled = this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
105
- const store = this.getAccountStore();
106
- const dbReady = !!store;
107
- const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false;
108
+ const accountState = await this.getPersistentAccountState();
109
+ const bootstrapAvailable = !accountState.dbEnabled || (accountState.dbReady && !accountState.hasPersistentAdmin);
108
110
  return {
109
- dbEnabled,
110
- dbReady,
111
- hasPersistentAdmin,
112
- needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
113
- ephemeralAdminAvailable: !hasPersistentAdmin,
111
+ dbEnabled: accountState.dbEnabled,
112
+ dbReady: accountState.dbReady,
113
+ hasPersistentAdmin: accountState.hasPersistentAdmin,
114
+ needsBootstrap: accountState.dbEnabled && accountState.dbReady && !accountState.hasPersistentAdmin,
115
+ ephemeralAdminAvailable: bootstrapAvailable,
114
116
  idpGlobalConfigured: this.isIdpGlobalConfigured(),
115
117
  };
116
118
  }
@@ -408,10 +410,14 @@ export class AdminHandler {
408
410
  password: string;
409
411
  authSource?: interfaces.requests.TAdminLoginAuthSource;
410
412
  }): Promise<TAdminUser | null> {
411
- if (await this.hasPersistentAdminAccount()) {
412
- const store = this.getAccountStore();
413
+ const accountState = await this.getPersistentAccountState();
414
+ if (accountState.dbEnabled && !accountState.dbReady) {
415
+ throw new plugins.typedrequest.TypedResponseError('database is not ready');
416
+ }
417
+
418
+ if (accountState.hasPersistentAdmin) {
413
419
  const authService = new plugins.idpSdkServer.AccountAuthService({
414
- store: store!,
420
+ store: accountState.store!,
415
421
  idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
416
422
  });
417
423
  const result = await authService.authenticate({
@@ -431,8 +437,13 @@ export class AdminHandler {
431
437
  }
432
438
 
433
439
  private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
434
- if (await this.hasPersistentAdminAccount()) {
435
- const account = await this.getAccountStore()!.getAccountById(userIdArg);
440
+ const accountState = await this.getPersistentAccountState();
441
+ if (accountState.dbEnabled && !accountState.dbReady) {
442
+ return null;
443
+ }
444
+
445
+ if (accountState.hasPersistentAdmin) {
446
+ const account = await accountState.store!.getAccountById(userIdArg);
436
447
  if (!account || account.status !== 'active') {
437
448
  return null;
438
449
  }
@@ -442,13 +453,25 @@ export class AdminHandler {
442
453
  return this.users.get(userIdArg) || null;
443
454
  }
444
455
 
445
- private async hasPersistentAdminAccount(): Promise<boolean> {
446
- const store = this.getAccountStore();
447
- return store ? store.hasActiveAdminAccount() : false;
456
+ private async getPersistentAccountState(): Promise<{
457
+ dbEnabled: boolean;
458
+ dbReady: boolean;
459
+ store: plugins.idpSdkServer.SmartdataAccountStore | null;
460
+ hasPersistentAdmin: boolean;
461
+ }> {
462
+ const dbEnabled = this.isPersistenceEnabled();
463
+ const store = dbEnabled ? this.getAccountStore() : null;
464
+ const dbReady = !!store;
465
+ const hasPersistentAdmin = store ? await store.hasActiveAdminAccount() : false;
466
+ return { dbEnabled, dbReady, store, hasPersistentAdmin };
467
+ }
468
+
469
+ private isPersistenceEnabled(): boolean {
470
+ return this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
448
471
  }
449
472
 
450
473
  private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
451
- if (this.opsServerRef.dcRouterRef.options.dbConfig?.enabled === false) {
474
+ if (!this.isPersistenceEnabled()) {
452
475
  return null;
453
476
  }
454
477
  const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
@@ -180,7 +180,14 @@ export class SecurityHandler {
180
180
  async (dataArg) => {
181
181
  await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
182
182
  const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
183
- return { records: manager ? await manager.listIpIntelligence() : [] };
183
+ return {
184
+ records: manager
185
+ ? await manager.listIpIntelligence({
186
+ ipAddresses: dataArg.ipAddresses,
187
+ limit: dataArg.limit,
188
+ })
189
+ : [],
190
+ };
184
191
  },
185
192
  ),
186
193
  );
@@ -19,12 +19,24 @@ export interface IRemoteIngressFirewallSnapshot {
19
19
  blockedIps: string[];
20
20
  }
21
21
 
22
+ const OBSERVED_IP_QUEUE_LIMIT = 512;
23
+ const OBSERVED_IP_BATCH_LIMIT = 20;
24
+ const OBSERVED_IP_QUEUE_CONCURRENCY = 2;
25
+ const OBSERVED_IP_REQUEUE_THROTTLE_MS = 60_000;
26
+
22
27
  export class SecurityPolicyManager {
23
28
  private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
24
29
  cacheTtl: 24 * 60 * 60 * 1000,
30
+ ipIntelligenceTimeout: 5_000,
25
31
  });
26
32
  private readonly intelligenceRefreshMs: number;
27
- private readonly inFlightObservations = new Set<string>();
33
+ private readonly inFlightObservations = new Map<string, Promise<void>>();
34
+ private readonly queuedObservations = new Set<string>();
35
+ private readonly observationQueue: string[] = [];
36
+ private readonly lastQueuedAt = new Map<string, number>();
37
+ private activeQueuedObservations = 0;
38
+ private queueDrainScheduled = false;
39
+ private isStopping = false;
28
40
  private readonly onPolicyChanged?: () => void | Promise<void>;
29
41
 
30
42
  constructor(options: ISecurityPolicyManagerOptions = {}) {
@@ -37,6 +49,9 @@ export class SecurityPolicyManager {
37
49
  }
38
50
 
39
51
  public async stop(): Promise<void> {
52
+ this.isStopping = true;
53
+ this.observationQueue.length = 0;
54
+ this.queuedObservations.clear();
40
55
  await this.smartNetwork.stop();
41
56
  }
42
57
 
@@ -45,13 +60,55 @@ export class SecurityPolicyManager {
45
60
  await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
46
61
  }
47
62
 
63
+ public queueObservedIps(ips: string[]): void {
64
+ if (this.isStopping) return;
65
+
66
+ const now = Date.now();
67
+ const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
68
+
69
+ for (const ip of uniqueIps.slice(0, OBSERVED_IP_BATCH_LIMIT)) {
70
+ if (!this.isPublicIp(ip)) continue;
71
+ if (this.inFlightObservations.has(ip) || this.queuedObservations.has(ip)) continue;
72
+
73
+ const lastQueuedAt = this.lastQueuedAt.get(ip);
74
+ if (lastQueuedAt && now - lastQueuedAt < OBSERVED_IP_REQUEUE_THROTTLE_MS) continue;
75
+
76
+ if (this.observationQueue.length >= OBSERVED_IP_QUEUE_LIMIT) {
77
+ const droppedIp = this.observationQueue.shift();
78
+ if (droppedIp) this.queuedObservations.delete(droppedIp);
79
+ }
80
+
81
+ this.observationQueue.push(ip);
82
+ this.queuedObservations.add(ip);
83
+ this.lastQueuedAt.set(ip, now);
84
+ }
85
+
86
+ this.pruneQueuedIpMemory(now);
87
+ this.scheduleQueueDrain();
88
+ }
89
+
48
90
  public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
49
91
  const ip = this.normalizeIp(ipAddress);
50
- if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
92
+ if (!ip || !this.isPublicIp(ip)) {
51
93
  return;
52
94
  }
53
95
 
54
- this.inFlightObservations.add(ip);
96
+ const existingObservation = this.inFlightObservations.get(ip);
97
+ if (existingObservation) {
98
+ await existingObservation;
99
+ if (!options.force) return;
100
+ }
101
+
102
+ const observationPromise = this.performObserveIp(ip, options).finally(() => {
103
+ if (this.inFlightObservations.get(ip) === observationPromise) {
104
+ this.inFlightObservations.delete(ip);
105
+ }
106
+ });
107
+ this.inFlightObservations.set(ip, observationPromise);
108
+ await observationPromise;
109
+ }
110
+
111
+ private async performObserveIp(ip: string, options: { force?: boolean } = {}): Promise<void> {
55
112
  try {
56
113
  const now = Date.now();
57
114
  let doc = await IpIntelligenceDoc.findByIp(ip);
@@ -81,8 +138,6 @@ export class SecurityPolicyManager {
81
138
  }
82
139
  } catch (err) {
83
140
  logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
84
- } finally {
85
- this.inFlightObservations.delete(ip);
86
141
  }
87
142
  }
88
143
 
@@ -90,8 +145,22 @@ export class SecurityPolicyManager {
90
145
  return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
91
146
  }
92
147
 
93
- public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
94
- return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc));
148
+ public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
149
+ const limit = Number.isInteger(options.limit) && options.limit! > 0
150
+ ? Math.min(options.limit!, 500)
151
+ : undefined;
152
+
153
+ let docs: IpIntelligenceDoc[];
154
+ if (options.ipAddresses?.length) {
155
+ const ips = [...new Set(options.ipAddresses.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
156
+ const results = await Promise.all(ips.map((ip) => IpIntelligenceDoc.findByIp(ip)));
157
+ docs = results.filter(Boolean) as IpIntelligenceDoc[];
158
+ } else {
159
+ docs = await IpIntelligenceDoc.findAll();
160
+ }
161
+
162
+ const sortedDocs = docs.sort((a, b) => (b.lastSeenAt || 0) - (a.lastSeenAt || 0));
163
+ return (limit ? sortedDocs.slice(0, limit) : sortedDocs).map((doc) => this.intelligenceFromDoc(doc));
95
164
  }
96
165
 
97
166
  public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
@@ -104,6 +173,45 @@ export class SecurityPolicyManager {
104
173
  return doc ? this.intelligenceFromDoc(doc) : null;
105
174
  }
106
175
 
176
+ private scheduleQueueDrain(): void {
177
+ if (this.queueDrainScheduled || this.isStopping) return;
178
+ this.queueDrainScheduled = true;
179
+ setTimeout(() => {
180
+ this.queueDrainScheduled = false;
181
+ this.drainObservationQueue();
182
+ }, 0);
183
+ }
184
+
185
+ private drainObservationQueue(): void {
186
+ if (this.isStopping) return;
187
+
188
+ while (
189
+ this.activeQueuedObservations < OBSERVED_IP_QUEUE_CONCURRENCY &&
190
+ this.observationQueue.length > 0
191
+ ) {
192
+ const ip = this.observationQueue.shift()!;
193
+ this.queuedObservations.delete(ip);
194
+ this.activeQueuedObservations++;
195
+ void this.observeIp(ip)
196
+ .catch(() => undefined)
197
+ .finally(() => {
198
+ this.activeQueuedObservations--;
199
+ if (this.observationQueue.length > 0) {
200
+ this.scheduleQueueDrain();
201
+ }
202
+ });
203
+ }
204
+ }
205
+
206
+ private pruneQueuedIpMemory(now: number): void {
207
+ if (this.lastQueuedAt.size <= OBSERVED_IP_QUEUE_LIMIT * 2) return;
208
+ for (const [ip, lastQueuedAt] of this.lastQueuedAt) {
209
+ if (now - lastQueuedAt > OBSERVED_IP_REQUEUE_THROTTLE_MS * 2) {
210
+ this.lastQueuedAt.delete(ip);
211
+ }
212
+ }
213
+ }
214
+
107
215
  public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
108
216
  return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
109
217
  id: doc.id,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.32.0',
6
+ version: '13.33.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -582,6 +582,52 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
582
582
  };
583
583
  });
584
584
 
585
+ const backgroundRefreshesInFlight = new Set<string>();
586
+
587
+ function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): void {
588
+ if (backgroundRefreshesInFlight.has(key)) return;
589
+ backgroundRefreshesInFlight.add(key);
590
+ void task()
591
+ .catch((error) => console.error(errorMessage, error))
592
+ .finally(() => backgroundRefreshesInFlight.delete(key));
593
+ }
594
+
595
+ function refreshNetworkIpIntelligence(identity: interfaces.data.IIdentity, ipAddresses: string[]): void {
596
+ const ips = [...new Set(ipAddresses.filter(Boolean))].slice(0, 100);
597
+ if (ips.length === 0) return;
598
+
599
+ runBackgroundRefresh('networkIpIntelligence', 'IP intelligence refresh failed:', async () => {
600
+ const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
601
+ interfaces.requests.IReq_ListIpIntelligence
602
+ >('/typedrequest', 'listIpIntelligence');
603
+ const intelligenceResponse = await intelligenceRequest.fire({
604
+ identity,
605
+ ipAddresses: ips,
606
+ limit: Math.max(100, ips.length),
607
+ });
608
+ networkStatePart.setState({
609
+ ...networkStatePart.getState()!,
610
+ ipIntelligence: intelligenceResponse.records || [],
611
+ });
612
+ });
613
+ }
614
+
615
+ function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void {
616
+ runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => {
617
+ const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
618
+ interfaces.requests.IReq_ListIpIntelligence
619
+ >('/typedrequest', 'listIpIntelligence');
620
+ const intelligenceResponse = await intelligenceRequest.fire({
621
+ identity,
622
+ limit: 500,
623
+ });
624
+ securityPolicyStatePart.setState({
625
+ ...securityPolicyStatePart.getState()!,
626
+ ipIntelligence: intelligenceResponse.records || [],
627
+ });
628
+ });
629
+ }
630
+
585
631
  // Fetch Network Stats Action
586
632
  export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
587
633
  const context = getActionContext();
@@ -594,18 +640,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
594
640
  interfaces.requests.IReq_GetNetworkStats
595
641
  >('/typedrequest', 'getNetworkStats');
596
642
 
597
- const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
598
- interfaces.requests.IReq_ListIpIntelligence
599
- >('/typedrequest', 'listIpIntelligence');
600
-
601
- const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
602
- networkStatsRequest.fire({
603
- identity: context.identity,
604
- }),
605
- ipIntelligenceRequest.fire({
606
- identity: context.identity,
607
- }),
608
- ]);
643
+ const networkStatsResponse = await networkStatsRequest.fire({
644
+ identity: context.identity,
645
+ });
609
646
 
610
647
  // Use the connections data for the connection list
611
648
  // and network stats for throughput and IP analytics
@@ -637,6 +674,12 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
637
674
  };
638
675
  });
639
676
 
677
+ refreshNetworkIpIntelligence(context.identity, [
678
+ ...Object.keys(connectionsByIP),
679
+ ...(networkStatsResponse.topIPs || []).map((item) => item.ip),
680
+ ...(networkStatsResponse.topIPsByBandwidth || []).map((item) => item.ip),
681
+ ]);
682
+
640
683
  return {
641
684
  connections,
642
685
  connectionsByIP,
@@ -647,7 +690,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
647
690
  topIPs: networkStatsResponse.topIPs || [],
648
691
  topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
649
692
  throughputByIP: networkStatsResponse.throughputByIP || [],
650
- ipIntelligence: ipIntelligenceResponse.records || [],
693
+ ipIntelligence: currentState.ipIntelligence,
651
694
  domainActivity: networkStatsResponse.domainActivity || [],
652
695
  throughputHistory: networkStatsResponse.throughputHistory || [],
653
696
  requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
@@ -683,9 +726,6 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
683
726
  const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
684
727
  interfaces.requests.IReq_ListSecurityBlockRules
685
728
  >('/typedrequest', 'listSecurityBlockRules');
686
- const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
687
- interfaces.requests.IReq_ListIpIntelligence
688
- >('/typedrequest', 'listIpIntelligence');
689
729
  const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
690
730
  interfaces.requests.IReq_GetCompiledSecurityPolicy
691
731
  >('/typedrequest', 'getCompiledSecurityPolicy');
@@ -693,16 +733,17 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
693
733
  interfaces.requests.IReq_ListSecurityPolicyAudit
694
734
  >('/typedrequest', 'listSecurityPolicyAudit');
695
735
 
696
- const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
736
+ const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
697
737
  rulesRequest.fire({ identity: context.identity }),
698
- intelligenceRequest.fire({ identity: context.identity }),
699
738
  compiledPolicyRequest.fire({ identity: context.identity }),
700
739
  auditRequest.fire({ identity: context.identity, limit: 100 }),
701
740
  ]);
702
741
 
742
+ refreshSecurityIpIntelligence(context.identity);
743
+
703
744
  return {
704
745
  rules: rulesResponse.rules || [],
705
- ipIntelligence: intelligenceResponse.records || [],
746
+ ipIntelligence: currentState.ipIntelligence,
706
747
  compiledPolicy: compiledPolicyResponse.policy,
707
748
  auditEvents: auditResponse.events || [],
708
749
  isLoading: false,
@@ -835,7 +876,15 @@ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<
835
876
  if (!response.success) {
836
877
  return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
837
878
  }
838
- return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
879
+ const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null);
880
+ if (!response.record) return refreshedState;
881
+ return {
882
+ ...refreshedState,
883
+ ipIntelligence: [
884
+ response.record,
885
+ ...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress),
886
+ ],
887
+ };
839
888
  } catch (error: unknown) {
840
889
  return {
841
890
  ...currentState,
@@ -3112,53 +3161,38 @@ async function dispatchCombinedRefreshActionInner() {
3112
3161
  error: null,
3113
3162
  });
3114
3163
 
3115
- try {
3116
- const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
3117
- interfaces.requests.IReq_ListIpIntelligence
3118
- >('/typedrequest', 'listIpIntelligence');
3119
- const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity });
3120
- networkStatePart.setState({
3121
- ...networkStatePart.getState()!,
3122
- ipIntelligence: intelligenceResponse.records || [],
3123
- });
3124
- } catch (error) {
3125
- console.error('IP intelligence refresh failed:', error);
3126
- }
3164
+ refreshNetworkIpIntelligence(context.identity, [
3165
+ ...network.connectionDetails.map((conn) => conn.remoteAddress),
3166
+ ...network.topEndpoints.map((endpoint) => endpoint.endpoint),
3167
+ ...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
3168
+ ]);
3127
3169
  }
3128
3170
 
3129
3171
  if (currentView === 'security') {
3130
- try {
3172
+ runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
3131
3173
  await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
3132
- } catch (error) {
3133
- console.error('Security policy refresh failed:', error);
3134
- }
3174
+ });
3135
3175
  }
3136
3176
 
3137
3177
  // Refresh certificate data if on Domains > Certificates subview
3138
3178
  if (currentView === 'domains' && currentSubview === 'certificates') {
3139
- try {
3179
+ runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
3140
3180
  await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
3141
- } catch (error) {
3142
- console.error('Certificate refresh failed:', error);
3143
- }
3181
+ });
3144
3182
  }
3145
3183
 
3146
3184
  // Refresh remote ingress data if on the Network → Remote Ingress subview
3147
3185
  if (currentView === 'network' && currentSubview === 'remoteingress') {
3148
- try {
3186
+ runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
3149
3187
  await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
3150
- } catch (error) {
3151
- console.error('Remote ingress refresh failed:', error);
3152
- }
3188
+ });
3153
3189
  }
3154
3190
 
3155
3191
  // Refresh VPN data if on the Network → VPN subview
3156
3192
  if (currentView === 'network' && currentSubview === 'vpn') {
3157
- try {
3193
+ runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
3158
3194
  await vpnStatePart.dispatchAction(fetchVpnAction, null);
3159
- } catch (error) {
3160
- console.error('VPN refresh failed:', error);
3161
- }
3195
+ });
3162
3196
  }
3163
3197
  } catch (error) {
3164
3198
  console.error('Combined refresh failed:', error);