@serve.zone/dcrouter 15.0.1 → 15.0.3

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.
Files changed (117) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +768 -768
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
  5. package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
  6. package/dist_ts/acme/index.d.ts +1 -0
  7. package/dist_ts/acme/index.js +2 -1
  8. package/dist_ts/classes.dcrouter.d.ts +21 -139
  9. package/dist_ts/classes.dcrouter.js +71 -1585
  10. package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
  11. package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
  12. package/dist_ts/dns/index.d.ts +1 -0
  13. package/dist_ts/dns/index.js +2 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
  15. package/dist_ts/email/classes.accepted-email-spool.js +345 -0
  16. package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
  17. package/dist_ts/email/classes.email-route-builder.js +260 -0
  18. package/dist_ts/email/index.d.ts +2 -0
  19. package/dist_ts/email/index.js +3 -1
  20. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
  22. package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
  23. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
  24. package/dist_ts/remoteingress/index.d.ts +1 -0
  25. package/dist_ts/remoteingress/index.js +2 -1
  26. package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
  27. package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
  28. package/dist_ts/security/index.d.ts +1 -0
  29. package/dist_ts/security/index.js +2 -1
  30. package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
  31. package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
  32. package/dist_ts/vpn/index.d.ts +1 -0
  33. package/dist_ts/vpn/index.js +2 -1
  34. package/dist_ts_migrations/index.js +92 -9
  35. package/dist_ts_web/00_commitinfo_data.js +1 -1
  36. package/dist_ts_web/appstate/acme.d.ts +17 -0
  37. package/dist_ts_web/appstate/acme.js +64 -0
  38. package/dist_ts_web/appstate/certificates.d.ts +37 -0
  39. package/dist_ts_web/appstate/certificates.js +107 -0
  40. package/dist_ts_web/appstate/config.d.ts +9 -0
  41. package/dist_ts_web/appstate/config.js +35 -0
  42. package/dist_ts_web/appstate/domains.d.ts +80 -0
  43. package/dist_ts_web/appstate/domains.js +324 -0
  44. package/dist_ts_web/appstate/email-domains.d.ts +25 -0
  45. package/dist_ts_web/appstate/email-domains.js +104 -0
  46. package/dist_ts_web/appstate/email-ops.d.ts +10 -0
  47. package/dist_ts_web/appstate/email-ops.js +40 -0
  48. package/dist_ts_web/appstate/login.d.ts +30 -0
  49. package/dist_ts_web/appstate/login.js +83 -0
  50. package/dist_ts_web/appstate/logs.d.ts +16 -0
  51. package/dist_ts_web/appstate/logs.js +27 -0
  52. package/dist_ts_web/appstate/network.d.ts +50 -0
  53. package/dist_ts_web/appstate/network.js +122 -0
  54. package/dist_ts_web/appstate/profiles-targets.d.ts +45 -0
  55. package/dist_ts_web/appstate/profiles-targets.js +173 -0
  56. package/dist_ts_web/appstate/remoteingress.d.ts +47 -0
  57. package/dist_ts_web/appstate/remoteingress.js +204 -0
  58. package/dist_ts_web/appstate/routes.d.ts +76 -0
  59. package/dist_ts_web/appstate/routes.js +316 -0
  60. package/dist_ts_web/appstate/runtime.d.ts +1 -0
  61. package/dist_ts_web/appstate/runtime.js +276 -0
  62. package/dist_ts_web/appstate/security.d.ts +29 -0
  63. package/dist_ts_web/appstate/security.js +167 -0
  64. package/dist_ts_web/appstate/shared.d.ts +3 -0
  65. package/dist_ts_web/appstate/shared.js +13 -0
  66. package/dist_ts_web/appstate/stats.d.ts +15 -0
  67. package/dist_ts_web/appstate/stats.js +59 -0
  68. package/dist_ts_web/appstate/target-profiles.d.ts +37 -0
  69. package/dist_ts_web/appstate/target-profiles.js +118 -0
  70. package/dist_ts_web/appstate/ui.d.ts +11 -0
  71. package/dist_ts_web/appstate/ui.js +55 -0
  72. package/dist_ts_web/appstate/users.d.ts +27 -0
  73. package/dist_ts_web/appstate/users.js +85 -0
  74. package/dist_ts_web/appstate/vpn.d.ts +44 -0
  75. package/dist_ts_web/appstate/vpn.js +148 -0
  76. package/dist_ts_web/appstate.d.ts +20 -568
  77. package/dist_ts_web/appstate.js +24 -2418
  78. package/package.json +1 -1
  79. package/ts/00_commitinfo_data.ts +1 -1
  80. package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
  81. package/ts/acme/index.ts +1 -0
  82. package/ts/classes.dcrouter.ts +118 -1919
  83. package/ts/dns/classes.dns-server-runtime.ts +525 -0
  84. package/ts/dns/index.ts +1 -0
  85. package/ts/email/classes.accepted-email-spool.ts +434 -0
  86. package/ts/email/classes.email-route-builder.ts +312 -0
  87. package/ts/email/index.ts +2 -0
  88. package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
  89. package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
  90. package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
  91. package/ts/remoteingress/index.ts +1 -0
  92. package/ts/security/classes.route-policy-augmenter.ts +140 -0
  93. package/ts/security/index.ts +1 -0
  94. package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
  95. package/ts/vpn/index.ts +1 -0
  96. package/ts_web/00_commitinfo_data.ts +1 -1
  97. package/ts_web/appstate/acme.ts +93 -0
  98. package/ts_web/appstate/certificates.ts +159 -0
  99. package/ts_web/appstate/config.ts +49 -0
  100. package/ts_web/appstate/domains.ts +429 -0
  101. package/ts_web/appstate/email-domains.ts +155 -0
  102. package/ts_web/appstate/email-ops.ts +57 -0
  103. package/ts_web/appstate/login.ts +128 -0
  104. package/ts_web/appstate/logs.ts +50 -0
  105. package/ts_web/appstate/network.ts +161 -0
  106. package/ts_web/appstate/profiles-targets.ts +240 -0
  107. package/ts_web/appstate/remoteingress.ts +300 -0
  108. package/ts_web/appstate/routes.ts +447 -0
  109. package/ts_web/appstate/runtime.ts +308 -0
  110. package/ts_web/appstate/security.ts +229 -0
  111. package/ts_web/appstate/shared.ts +15 -0
  112. package/ts_web/appstate/stats.ts +79 -0
  113. package/ts_web/appstate/target-profiles.ts +164 -0
  114. package/ts_web/appstate/ui.ts +75 -0
  115. package/ts_web/appstate/users.ts +133 -0
  116. package/ts_web/appstate/vpn.ts +234 -0
  117. package/ts_web/appstate.ts +24 -3403
@@ -0,0 +1,308 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as interfaces from '../../ts_interfaces/index.js';
3
+ import { runBackgroundRefresh } from './shared.js';
4
+ import { getActionContext, loginStatePart, logoutAction } from './login.js';
5
+ import { uiStatePart } from './ui.js';
6
+ import { statsStatePart } from './stats.js';
7
+ import { logStatePart, type ILogState } from './logs.js';
8
+ import { networkStatePart, refreshNetworkIpIntelligence } from './network.js';
9
+ import { securityPolicyStatePart, fetchSecurityPolicyAction } from './security.js';
10
+ import { certificateStatePart, fetchCertificateOverviewAction } from './certificates.js';
11
+ import { remoteIngressStatePart, fetchRemoteIngressAction } from './remoteingress.js';
12
+ import { vpnStatePart, fetchVpnAction } from './vpn.js';
13
+
14
+
15
+ // ============================================================================
16
+ // TypedSocket Client for Real-time Log Streaming
17
+ // ============================================================================
18
+
19
+ let socketClient: plugins.typedsocket.TypedSocket | null = null;
20
+ const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
21
+
22
+ // Batched log entry handler — buffers incoming entries and flushes once per animation frame
23
+ let logEntryBuffer: interfaces.data.ILogEntry[] = [];
24
+ let logFlushScheduled = false;
25
+
26
+ function flushLogEntries() {
27
+ logFlushScheduled = false;
28
+ if (logEntryBuffer.length === 0) return;
29
+ const current = logStatePart.getState()!;
30
+ const updated = [...current.recentLogs, ...logEntryBuffer];
31
+ logEntryBuffer = [];
32
+ // Cap at 2000 entries
33
+ if (updated.length > 2000) {
34
+ updated.splice(0, updated.length - 2000);
35
+ }
36
+ logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
37
+ }
38
+
39
+ // Register handler for pushed log entries from the server
40
+ socketRouter.addTypedHandler(
41
+ new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
42
+ 'pushLogEntry',
43
+ async (dataArg) => {
44
+ logEntryBuffer.push(dataArg.entry);
45
+ if (!logFlushScheduled) {
46
+ logFlushScheduled = true;
47
+ requestAnimationFrame(flushLogEntries);
48
+ }
49
+ return {};
50
+ }
51
+ )
52
+ );
53
+
54
+ async function connectSocket() {
55
+ if (socketClient) return;
56
+ try {
57
+ socketClient = await plugins.typedsocket.TypedSocket.createClient(
58
+ socketRouter,
59
+ plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
60
+ );
61
+ await socketClient.setTag('role', 'ops_dashboard');
62
+ } catch (err) {
63
+ console.error('TypedSocket connection failed:', err);
64
+ socketClient = null;
65
+ }
66
+ }
67
+
68
+ async function disconnectSocket() {
69
+ if (socketClient) {
70
+ try {
71
+ await socketClient.stop();
72
+ } catch {
73
+ // ignore disconnect errors
74
+ }
75
+ socketClient = null;
76
+ }
77
+ }
78
+
79
+ // In-flight guard to prevent concurrent refresh requests
80
+ let isRefreshing = false;
81
+
82
+ // Combined refresh action for efficient polling
83
+ async function dispatchCombinedRefreshAction() {
84
+ if (isRefreshing) return;
85
+ isRefreshing = true;
86
+ try {
87
+ await dispatchCombinedRefreshActionInner();
88
+ } finally {
89
+ isRefreshing = false;
90
+ }
91
+ }
92
+
93
+ async function dispatchCombinedRefreshActionInner() {
94
+ const context = getActionContext();
95
+ if (!context.identity) return;
96
+ const currentView = uiStatePart.getState()!.activeView;
97
+ const currentSubview = uiStatePart.getState()!.activeSubview;
98
+
99
+ try {
100
+ // Always fetch basic stats for dashboard widgets
101
+ const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
102
+ interfaces.requests.IReq_GetCombinedMetrics
103
+ >('/typedrequest', 'getCombinedMetrics');
104
+
105
+ const combinedResponse = await combinedRequest.fire({
106
+ identity: context.identity,
107
+ sections: {
108
+ server: true,
109
+ email: true,
110
+ dns: true,
111
+ security: true,
112
+ network: currentView === 'network' && currentSubview === 'activity',
113
+ radius: true,
114
+ vpn: true,
115
+ },
116
+ });
117
+
118
+ // Update all stats from combined response
119
+ const currentStatsState = statsStatePart.getState()!;
120
+ statsStatePart.setState({
121
+ ...currentStatsState,
122
+ serverStats: combinedResponse.metrics.server || currentStatsState.serverStats,
123
+ emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
124
+ dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
125
+ securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
126
+ radiusStats: combinedResponse.metrics.radius || currentStatsState.radiusStats,
127
+ vpnStats: combinedResponse.metrics.vpn || currentStatsState.vpnStats,
128
+ lastUpdated: Date.now(),
129
+ isLoading: false,
130
+ error: null,
131
+ });
132
+
133
+ // Update network stats if included
134
+ if (combinedResponse.metrics.network && currentView === 'network') {
135
+ const network = combinedResponse.metrics.network;
136
+ const connectionsByIP: { [ip: string]: number } = {};
137
+
138
+ // Build connectionsByIP from connectionDetails (now populated with real per-IP data)
139
+ network.connectionDetails.forEach(conn => {
140
+ connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + (conn.connectionCount || 1);
141
+ });
142
+
143
+ // Build connections from connectionDetails (real per-IP aggregates)
144
+ const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({
145
+ id: `ip-${conn.remoteAddress}`,
146
+ remoteAddress: conn.remoteAddress,
147
+ localAddress: 'server',
148
+ startTime: conn.startTime,
149
+ protocol: conn.protocol as any,
150
+ state: conn.state as any,
151
+ bytesReceived: conn.bytesIn,
152
+ bytesSent: conn.bytesOut,
153
+ connectionCount: conn.connectionCount,
154
+ }));
155
+
156
+ networkStatePart.setState({
157
+ ...networkStatePart.getState()!,
158
+ connections,
159
+ connectionsByIP,
160
+ throughputRate: {
161
+ bytesInPerSecond: network.totalBandwidth.in,
162
+ bytesOutPerSecond: network.totalBandwidth.out,
163
+ },
164
+ totalBytes: network.totalBytes || { in: 0, out: 0 },
165
+ topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })),
166
+ topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({
167
+ ip: e.endpoint,
168
+ count: e.connections,
169
+ bwIn: e.bandwidth?.in || 0,
170
+ bwOut: e.bandwidth?.out || 0,
171
+ })),
172
+ topASNs: network.topASNs || [],
173
+ throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
174
+ domainActivity: network.domainActivity || [],
175
+ throughputHistory: network.throughputHistory || [],
176
+ requestsPerSecond: network.requestsPerSecond || 0,
177
+ requestsTotal: network.requestsTotal || 0,
178
+ backends: network.backends || [],
179
+ frontendProtocols: network.frontendProtocols || null,
180
+ backendProtocols: network.backendProtocols || null,
181
+ lastUpdated: Date.now(),
182
+ isLoading: false,
183
+ error: null,
184
+ });
185
+
186
+ refreshNetworkIpIntelligence(context.identity, [
187
+ ...network.connectionDetails.map((conn) => conn.remoteAddress),
188
+ ...network.topEndpoints.map((endpoint) => endpoint.endpoint),
189
+ ...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
190
+ ]);
191
+ }
192
+
193
+ if (currentView === 'security') {
194
+ runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
195
+ await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
196
+ });
197
+ }
198
+
199
+ // Refresh certificate data if on Domains > Certificates subview
200
+ if (currentView === 'domains' && currentSubview === 'certificates') {
201
+ runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
202
+ await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
203
+ });
204
+ }
205
+
206
+ // Refresh remote ingress data if on the Network → Remote Ingress subview
207
+ if (currentView === 'network' && currentSubview === 'remoteingress') {
208
+ runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
209
+ await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
210
+ });
211
+ }
212
+
213
+ // Refresh VPN data if on the Network → VPN subview
214
+ if (currentView === 'network' && currentSubview === 'vpn') {
215
+ runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
216
+ await vpnStatePart.dispatchAction(fetchVpnAction, null);
217
+ });
218
+ }
219
+ } catch (error) {
220
+ console.error('Combined refresh failed:', error);
221
+ // If the error looks like an auth failure (invalid JWT), force re-login
222
+ const errMsg = String(error);
223
+ if (errMsg.includes('invalid') || errMsg.includes('unauthorized') || errMsg.includes('401')) {
224
+ await loginStatePart.dispatchAction(logoutAction, null);
225
+ window.location.reload();
226
+ }
227
+ }
228
+ }
229
+
230
+ // Create a proper action for the combined refresh so we can use createScheduledAction
231
+ const combinedRefreshAction = statsStatePart.createAction<void>(async (statePartArg) => {
232
+ await dispatchCombinedRefreshAction();
233
+ // Return current state — dispatchCombinedRefreshAction already updates all state parts directly
234
+ return statePartArg.getState()!;
235
+ });
236
+
237
+ // Scheduled refresh process with autoPause: 'visibility' — automatically pauses when tab is hidden
238
+ let refreshProcess: ReturnType<typeof statsStatePart.createScheduledAction> | null = null;
239
+
240
+ const startAutoRefresh = () => {
241
+ const uiState = uiStatePart.getState()!;
242
+ const loginState = loginStatePart.getState()!;
243
+
244
+ if (uiState.autoRefresh && loginState.isLoggedIn) {
245
+ // Dispose old process if interval changed or not running
246
+ if (refreshProcess) {
247
+ refreshProcess.dispose();
248
+ refreshProcess = null;
249
+ }
250
+ refreshProcess = statsStatePart.createScheduledAction({
251
+ action: combinedRefreshAction,
252
+ payload: undefined,
253
+ intervalMs: uiState.refreshInterval,
254
+ autoPause: 'visibility',
255
+ });
256
+ } else {
257
+ if (refreshProcess) {
258
+ refreshProcess.dispose();
259
+ refreshProcess = null;
260
+ }
261
+ }
262
+ };
263
+
264
+ // Watch for relevant changes
265
+ let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
266
+ let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
267
+ let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
268
+
269
+ uiStatePart.select((s) => ({ autoRefresh: s.autoRefresh, refreshInterval: s.refreshInterval }))
270
+ .subscribe((state) => {
271
+ if (state.autoRefresh !== previousAutoRefresh ||
272
+ state.refreshInterval !== previousRefreshInterval) {
273
+ previousAutoRefresh = state.autoRefresh;
274
+ previousRefreshInterval = state.refreshInterval;
275
+ startAutoRefresh();
276
+ }
277
+ });
278
+
279
+ loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
280
+ if (isLoggedIn !== previousIsLoggedIn) {
281
+ previousIsLoggedIn = isLoggedIn;
282
+ startAutoRefresh();
283
+
284
+ // Connect/disconnect TypedSocket based on login state
285
+ if (isLoggedIn) {
286
+ connectSocket();
287
+ } else {
288
+ disconnectSocket();
289
+ }
290
+ }
291
+ });
292
+
293
+ // Pause/resume WebSocket when tab visibility changes
294
+ document.addEventListener('visibilitychange', () => {
295
+ if (document.hidden) {
296
+ disconnectSocket();
297
+ } else if (loginStatePart.getState()!.isLoggedIn) {
298
+ connectSocket();
299
+ }
300
+ });
301
+
302
+ // Initial start
303
+ startAutoRefresh();
304
+
305
+ // Connect TypedSocket if already logged in (e.g., persistent session)
306
+ if (loginStatePart.getState()!.isLoggedIn) {
307
+ connectSocket();
308
+ }
@@ -0,0 +1,229 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as interfaces from '../../ts_interfaces/index.js';
3
+ import { appState } from './shared.js';
4
+ import { getActionContext } from './login.js';
5
+ import { runBackgroundRefresh } from './shared.js';
6
+
7
+ export interface ISecurityPolicyState {
8
+ rules: interfaces.data.ISecurityBlockRule[];
9
+ ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
10
+ compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
11
+ auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
12
+ isLoading: boolean;
13
+ error: string | null;
14
+ lastUpdated: number;
15
+ }
16
+
17
+ export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
18
+ 'securityPolicy',
19
+ {
20
+ rules: [],
21
+ ipIntelligence: [],
22
+ compiledPolicy: null,
23
+ auditEvents: [],
24
+ isLoading: false,
25
+ error: null,
26
+ lastUpdated: 0,
27
+ },
28
+ 'soft',
29
+ );
30
+
31
+ function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void {
32
+ runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => {
33
+ const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
34
+ interfaces.requests.IReq_ListIpIntelligence
35
+ >('/typedrequest', 'listIpIntelligence');
36
+ const intelligenceResponse = await intelligenceRequest.fire({
37
+ identity,
38
+ limit: 500,
39
+ });
40
+ securityPolicyStatePart.setState({
41
+ ...securityPolicyStatePart.getState()!,
42
+ ipIntelligence: intelligenceResponse.records || [],
43
+ });
44
+ });
45
+ }
46
+
47
+
48
+ // Security Policy Actions
49
+ // ============================================================================
50
+
51
+ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
52
+ async (statePartArg): Promise<ISecurityPolicyState> => {
53
+ const context = getActionContext();
54
+ const currentState = statePartArg.getState()!;
55
+ if (!context.identity) return currentState;
56
+
57
+ try {
58
+ const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
59
+ interfaces.requests.IReq_ListSecurityBlockRules
60
+ >('/typedrequest', 'listSecurityBlockRules');
61
+ const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
62
+ interfaces.requests.IReq_GetCompiledSecurityPolicy
63
+ >('/typedrequest', 'getCompiledSecurityPolicy');
64
+ const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
65
+ interfaces.requests.IReq_ListSecurityPolicyAudit
66
+ >('/typedrequest', 'listSecurityPolicyAudit');
67
+
68
+ const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
69
+ rulesRequest.fire({ identity: context.identity }),
70
+ compiledPolicyRequest.fire({ identity: context.identity }),
71
+ auditRequest.fire({ identity: context.identity, limit: 100 }),
72
+ ]);
73
+
74
+ refreshSecurityIpIntelligence(context.identity);
75
+
76
+ return {
77
+ rules: rulesResponse.rules || [],
78
+ ipIntelligence: currentState.ipIntelligence,
79
+ compiledPolicy: compiledPolicyResponse.policy,
80
+ auditEvents: auditResponse.events || [],
81
+ isLoading: false,
82
+ error: null,
83
+ lastUpdated: Date.now(),
84
+ };
85
+ } catch (error: unknown) {
86
+ return {
87
+ ...currentState,
88
+ isLoading: false,
89
+ error: error instanceof Error ? error.message : 'Failed to fetch security policy',
90
+ };
91
+ }
92
+ },
93
+ );
94
+
95
+ export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
96
+ type: interfaces.data.TSecurityBlockRuleType;
97
+ value: string;
98
+ matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
99
+ reason?: string;
100
+ enabled?: boolean;
101
+ }>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
102
+ const context = getActionContext();
103
+ const currentState = statePartArg.getState()!;
104
+ if (!context.identity) return currentState;
105
+
106
+ try {
107
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
108
+ interfaces.requests.IReq_CreateSecurityBlockRule
109
+ >('/typedrequest', 'createSecurityBlockRule');
110
+
111
+ const response = await request.fire({
112
+ identity: context.identity,
113
+ type: dataArg.type,
114
+ value: dataArg.value,
115
+ matchMode: dataArg.matchMode,
116
+ reason: dataArg.reason,
117
+ enabled: dataArg.enabled,
118
+ });
119
+
120
+ if (!response.success) {
121
+ return { ...currentState, error: response.message || 'Failed to create security block rule' };
122
+ }
123
+
124
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
125
+ } catch (error: unknown) {
126
+ return {
127
+ ...currentState,
128
+ error: error instanceof Error ? error.message : 'Failed to create security block rule',
129
+ };
130
+ }
131
+ });
132
+
133
+ export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
134
+ id: string;
135
+ value?: string;
136
+ matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
137
+ reason?: string;
138
+ enabled?: boolean;
139
+ }>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
140
+ const context = getActionContext();
141
+ const currentState = statePartArg.getState()!;
142
+ if (!context.identity) return currentState;
143
+
144
+ try {
145
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
146
+ interfaces.requests.IReq_UpdateSecurityBlockRule
147
+ >('/typedrequest', 'updateSecurityBlockRule');
148
+
149
+ const response = await request.fire({
150
+ identity: context.identity,
151
+ id: dataArg.id,
152
+ value: dataArg.value,
153
+ matchMode: dataArg.matchMode,
154
+ reason: dataArg.reason,
155
+ enabled: dataArg.enabled,
156
+ });
157
+
158
+ if (!response.success) {
159
+ return { ...currentState, error: response.message || 'Failed to update security block rule' };
160
+ }
161
+
162
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
163
+ } catch (error: unknown) {
164
+ return {
165
+ ...currentState,
166
+ error: error instanceof Error ? error.message : 'Failed to update security block rule',
167
+ };
168
+ }
169
+ });
170
+
171
+ export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
172
+ async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
173
+ const context = getActionContext();
174
+ const currentState = statePartArg.getState()!;
175
+ if (!context.identity) return currentState;
176
+
177
+ try {
178
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
179
+ interfaces.requests.IReq_DeleteSecurityBlockRule
180
+ >('/typedrequest', 'deleteSecurityBlockRule');
181
+
182
+ const response = await request.fire({ identity: context.identity, id: ruleId });
183
+ if (!response.success) {
184
+ return { ...currentState, error: response.message || 'Failed to delete security block rule' };
185
+ }
186
+
187
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
188
+ } catch (error: unknown) {
189
+ return {
190
+ ...currentState,
191
+ error: error instanceof Error ? error.message : 'Failed to delete security block rule',
192
+ };
193
+ }
194
+ },
195
+ );
196
+
197
+ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
198
+ async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
199
+ const context = getActionContext();
200
+ const currentState = statePartArg.getState()!;
201
+ if (!context.identity) return currentState;
202
+
203
+ try {
204
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
205
+ interfaces.requests.IReq_RefreshIpIntelligence
206
+ >('/typedrequest', 'refreshIpIntelligence');
207
+ const response = await request.fire({ identity: context.identity, ipAddress });
208
+ if (!response.success) {
209
+ return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
210
+ }
211
+ const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null);
212
+ if (!response.record) return refreshedState;
213
+ return {
214
+ ...refreshedState,
215
+ ipIntelligence: [
216
+ response.record,
217
+ ...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress),
218
+ ],
219
+ };
220
+ } catch (error: unknown) {
221
+ return {
222
+ ...currentState,
223
+ error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
224
+ };
225
+ }
226
+ },
227
+ );
228
+
229
+ // ============================================================================
@@ -0,0 +1,15 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ // Create main app state instance
4
+ export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
5
+
6
+ const backgroundRefreshesInFlight = new Set<string>();
7
+
8
+ export function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): void {
9
+ if (backgroundRefreshesInFlight.has(key)) return;
10
+ backgroundRefreshesInFlight.add(key);
11
+ void task()
12
+ .catch((error) => console.error(errorMessage, error))
13
+ .finally(() => backgroundRefreshesInFlight.delete(key));
14
+ }
15
+
@@ -0,0 +1,79 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as interfaces from '../../ts_interfaces/index.js';
3
+ import { appState } from './shared.js';
4
+ import { getActionContext } from './login.js';
5
+
6
+ export interface IStatsState {
7
+ serverStats: interfaces.data.IServerStats | null;
8
+ emailStats: interfaces.data.IEmailStats | null;
9
+ dnsStats: interfaces.data.IDnsStats | null;
10
+ securityMetrics: interfaces.data.ISecurityMetrics | null;
11
+ radiusStats: interfaces.data.IRadiusStats | null;
12
+ vpnStats: interfaces.data.IVpnStats | null;
13
+ lastUpdated: number;
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ }
17
+
18
+ export const statsStatePart = await appState.getStatePart<IStatsState>(
19
+ 'stats',
20
+ {
21
+ serverStats: null,
22
+ emailStats: null,
23
+ dnsStats: null,
24
+ securityMetrics: null,
25
+ radiusStats: null,
26
+ vpnStats: null,
27
+ lastUpdated: 0,
28
+ isLoading: false,
29
+ error: null,
30
+ },
31
+ 'soft' // Stats are cached but not persisted
32
+ );
33
+
34
+ // Fetch All Stats Action - Using combined endpoint for efficiency
35
+ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg): Promise<IStatsState> => {
36
+ const context = getActionContext();
37
+ const currentState = statePartArg.getState()!;
38
+ if (!context.identity) return currentState;
39
+
40
+ try {
41
+ // Use combined metrics endpoint - single request instead of 4
42
+ const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
43
+ interfaces.requests.IReq_GetCombinedMetrics
44
+ >('/typedrequest', 'getCombinedMetrics');
45
+
46
+ const combinedResponse = await combinedRequest.fire({
47
+ identity: context.identity,
48
+ sections: {
49
+ server: true,
50
+ email: true,
51
+ dns: true,
52
+ security: true,
53
+ network: false, // Network is fetched separately for the network view
54
+ radius: true,
55
+ vpn: true,
56
+ },
57
+ });
58
+
59
+ // Update state with all stats from combined response
60
+ return {
61
+ serverStats: combinedResponse.metrics.server || currentState.serverStats,
62
+ emailStats: combinedResponse.metrics.email || currentState.emailStats,
63
+ dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
64
+ securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
65
+ radiusStats: combinedResponse.metrics.radius || currentState.radiusStats,
66
+ vpnStats: combinedResponse.metrics.vpn || currentState.vpnStats,
67
+ lastUpdated: Date.now(),
68
+ isLoading: false,
69
+ error: null,
70
+ };
71
+ } catch (error: unknown) {
72
+ return {
73
+ ...currentState,
74
+ isLoading: false,
75
+ error: (error as Error).message || 'Failed to fetch statistics',
76
+ };
77
+ }
78
+ });
79
+