@serve.zone/dcrouter 18.0.2 → 18.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1529 -1382
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +7 -2
- package/dist_ts/classes.dcrouter.js +61 -9
- package/dist_ts/db/documents/classes.authentication-event.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.authentication-event.doc.js +273 -0
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +200 -0
- package/dist_ts/db/documents/classes.smtp-account.doc.d.ts +31 -0
- package/dist_ts/db/documents/classes.smtp-account.doc.js +176 -0
- package/dist_ts/db/documents/index.d.ts +3 -0
- package/dist_ts/db/documents/index.js +4 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -0
- package/dist_ts/email/classes.accepted-email-spool.js +9 -1
- package/dist_ts/email/classes.email-settings.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-settings.manager.js +30 -1
- package/dist_ts/email/classes.smtp-account.manager.d.ts +110 -0
- package/dist_ts/email/classes.smtp-account.manager.js +507 -0
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +23 -4
- package/dist_ts/email/classes.workapp-mail-manager.js +39 -46
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/smtp-scram.d.ts +13 -0
- package/dist_ts/email/smtp-scram.js +19 -0
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +19 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +240 -44
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/admin.handler.js +61 -14
- package/dist_ts/opsserver/handlers/email-settings.handler.js +14 -2
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/security.handler.js +21 -3
- package/dist_ts/opsserver/handlers/smtp-account.handler.d.ts +20 -0
- package/dist_ts/opsserver/handlers/smtp-account.handler.js +155 -0
- package/dist_ts/opsserver/handlers/stats.handler.js +27 -10
- package/dist_ts/security/classes.authentication-event-manager.d.ts +35 -0
- package/dist_ts/security/classes.authentication-event-manager.js +185 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts_interfaces/data/email-settings.d.ts +10 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +1 -1
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/data/smtp-account.d.ts +50 -0
- package/dist_ts_interfaces/data/smtp-account.js +9 -0
- package/dist_ts_interfaces/data/stats.d.ts +16 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/smtp-accounts.d.ts +125 -0
- package/dist_ts_interfaces/requests/smtp-accounts.js +2 -0
- package/dist_ts_migrations/index.js +186 -13
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/smtp-accounts.d.ts +38 -0
- package/dist_ts_web/appstate/smtp-accounts.js +94 -0
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/index.d.ts +1 -0
- package/dist_ts_web/elements/email/index.js +2 -1
- package/dist_ts_web/elements/email/ops-view-smtp-accounts.d.ts +25 -0
- package/dist_ts_web/elements/email/ops-view-smtp-accounts.js +598 -0
- package/dist_ts_web/elements/ops-dashboard.js +3 -1
- package/dist_ts_web/elements/overview/ops-view-overview.js +3 -2
- package/dist_ts_web/elements/security/ops-view-security-authentication.js +20 -13
- package/dist_ts_web/elements/security/ops-view-security-overview.js +8 -11
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/readme.md +20 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +66 -8
- package/ts/db/documents/classes.authentication-event.doc.ts +248 -0
- package/ts/db/documents/classes.email-traffic-bucket.doc.ts +161 -0
- package/ts/db/documents/classes.smtp-account.doc.ts +103 -0
- package/ts/db/documents/index.ts +3 -0
- package/ts/email/classes.accepted-email-spool.ts +8 -0
- package/ts/email/classes.email-settings.manager.ts +34 -0
- package/ts/email/classes.smtp-account.manager.ts +601 -0
- package/ts/email/classes.workapp-mail-manager.ts +53 -55
- package/ts/email/index.ts +2 -0
- package/ts/email/smtp-scram.ts +24 -0
- package/ts/monitoring/classes.metricsmanager.ts +287 -50
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/admin.handler.ts +74 -15
- package/ts/opsserver/handlers/email-settings.handler.ts +13 -1
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/security.handler.ts +23 -2
- package/ts/opsserver/handlers/smtp-account.handler.ts +199 -0
- package/ts/opsserver/handlers/stats.handler.ts +29 -9
- package/ts/security/classes.authentication-event-manager.ts +221 -0
- package/ts/security/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/smtp-accounts.ts +143 -0
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/index.ts +1 -0
- package/ts_web/elements/email/ops-view-smtp-accounts.ts +586 -0
- package/ts_web/elements/ops-dashboard.ts +2 -0
- package/ts_web/elements/overview/ops-view-overview.ts +2 -1
- package/ts_web/elements/security/ops-view-security-authentication.ts +20 -13
- package/ts_web/elements/security/ops-view-security-overview.ts +7 -12
- package/ts_web/router.ts +1 -1
|
@@ -93,6 +93,26 @@ interface IStoredWorkAppMailState {
|
|
|
93
93
|
identities: IStoredWorkAppMailIdentity[];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* WorkApp-generated routes and SMTP users contributed to the composed email
|
|
98
|
+
* runtime config. Consumed by SmtpAccountManager, the single owner of the
|
|
99
|
+
* final `auth` block and route-list composition.
|
|
100
|
+
*/
|
|
101
|
+
export interface IWorkAppMailRuntimeContribution {
|
|
102
|
+
routes: IEmailRoute[];
|
|
103
|
+
users: Array<{ username: string; password: string }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Route names generated (and owned wholesale) by WorkAppMailManager. */
|
|
107
|
+
export function isWorkAppManagedMailRouteName(routeName: string): boolean {
|
|
108
|
+
return routeName.startsWith('workapp-mail-');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** SMTP usernames generated (and owned wholesale) by WorkAppMailManager. */
|
|
112
|
+
export function isWorkAppManagedSmtpUsername(username: string): boolean {
|
|
113
|
+
return username.startsWith('workapp-');
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
export class WorkAppMailManager {
|
|
97
117
|
private readonly storageKey = '/workhosters/mail-identities.json';
|
|
98
118
|
private identityMutationPromise: Promise<unknown> = Promise.resolve();
|
|
@@ -659,12 +679,36 @@ export class WorkAppMailManager {
|
|
|
659
679
|
};
|
|
660
680
|
}
|
|
661
681
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Compute the workapp-generated routes and SMTP users for the given
|
|
684
|
+
* candidate email config. Pure contribution — the final composition of
|
|
685
|
+
* routes and the `auth` block is owned exclusively by SmtpAccountManager.
|
|
686
|
+
*/
|
|
687
|
+
public async getStoredIdentityContribution(
|
|
688
|
+
emailConfig: IUnifiedEmailServerOptions,
|
|
689
|
+
identities = undefined as IStoredWorkAppMailIdentity[] | undefined,
|
|
690
|
+
): Promise<IWorkAppMailRuntimeContribution> {
|
|
691
|
+
const nextIdentities = identities || await this.readStoredIdentities();
|
|
692
|
+
const readyDomains = await this.getReadyOutboundDomains(nextIdentities);
|
|
693
|
+
const generatedRoutes = nextIdentities
|
|
694
|
+
.filter((identity) => {
|
|
695
|
+
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
696
|
+
return identity.enabled && ['smtpForward', 'typedEndpoint'].includes(inboundTarget?.type || '');
|
|
697
|
+
})
|
|
698
|
+
.map((identity) => this.buildInboundRoute(identity));
|
|
699
|
+
const generatedOutboundRoutes = nextIdentities
|
|
700
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
701
|
+
.map((identity) => this.buildOutboundRoute(identity, emailConfig));
|
|
702
|
+
const generatedUsers = nextIdentities
|
|
703
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
704
|
+
.map((identity) => ({
|
|
705
|
+
username: identity.smtp.username,
|
|
706
|
+
password: identity.smtpPassword,
|
|
707
|
+
}));
|
|
708
|
+
return {
|
|
709
|
+
routes: [...generatedRoutes, ...generatedOutboundRoutes],
|
|
710
|
+
users: generatedUsers,
|
|
711
|
+
};
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
public async applyStoredIdentitiesToRuntime(
|
|
@@ -673,15 +717,8 @@ export class WorkAppMailManager {
|
|
|
673
717
|
const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
|
|
674
718
|
if (!emailConfig) return;
|
|
675
719
|
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
const nextConfig = this.mergeIdentitiesIntoEmailConfig(emailConfig, nextIdentities, readyDomains);
|
|
679
|
-
|
|
680
|
-
this.dcRouterRef.options.emailConfig = nextConfig;
|
|
681
|
-
if (this.dcRouterRef.emailServer) {
|
|
682
|
-
this.dcRouterRef.emailServer.updateOptions({ auth: nextConfig.auth });
|
|
683
|
-
await this.dcRouterRef.updateEmailRoutes(nextConfig.routes);
|
|
684
|
-
}
|
|
720
|
+
const contribution = await this.getStoredIdentityContribution(emailConfig, identities);
|
|
721
|
+
await this.dcRouterRef.smtpAccountManager.applyToRuntime(contribution);
|
|
685
722
|
}
|
|
686
723
|
|
|
687
724
|
private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
|
|
@@ -717,41 +754,6 @@ export class WorkAppMailManager {
|
|
|
717
754
|
>;
|
|
718
755
|
}
|
|
719
756
|
|
|
720
|
-
private mergeIdentitiesIntoEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
|
721
|
-
emailConfig: TConfig,
|
|
722
|
-
identities: IStoredWorkAppMailIdentity[],
|
|
723
|
-
readyDomains: Set<string>,
|
|
724
|
-
): TConfig {
|
|
725
|
-
const generatedRoutes = identities
|
|
726
|
-
.filter((identity) => {
|
|
727
|
-
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
728
|
-
return identity.enabled && ['smtpForward', 'typedEndpoint'].includes(inboundTarget?.type || '');
|
|
729
|
-
})
|
|
730
|
-
.map((identity) => this.buildInboundRoute(identity));
|
|
731
|
-
const generatedOutboundRoutes = identities
|
|
732
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
733
|
-
.map((identity) => this.buildOutboundRoute(identity, emailConfig));
|
|
734
|
-
const configuredRoutes = (emailConfig.routes || [])
|
|
735
|
-
.filter((route) => !this.isManagedMailRouteName(route.name));
|
|
736
|
-
const generatedUsers = identities
|
|
737
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
738
|
-
.map((identity) => ({
|
|
739
|
-
username: identity.smtp.username,
|
|
740
|
-
password: identity.smtpPassword,
|
|
741
|
-
}));
|
|
742
|
-
const configuredUsers = (emailConfig.auth?.users || [])
|
|
743
|
-
.filter((user) => !this.isManagedSmtpUsername(user.username));
|
|
744
|
-
|
|
745
|
-
return {
|
|
746
|
-
...emailConfig,
|
|
747
|
-
routes: [...configuredRoutes, ...generatedRoutes, ...generatedOutboundRoutes],
|
|
748
|
-
auth: {
|
|
749
|
-
...(emailConfig.auth || {}),
|
|
750
|
-
users: [...configuredUsers, ...generatedUsers],
|
|
751
|
-
},
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
757
|
private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
|
|
756
758
|
const inboundTarget = this.getCanonicalInboundTarget(identity);
|
|
757
759
|
if (inboundTarget?.type === 'typedEndpoint') {
|
|
@@ -1594,12 +1596,8 @@ export class WorkAppMailManager {
|
|
|
1594
1596
|
return plugins.crypto.randomBytes(24).toString('base64url');
|
|
1595
1597
|
}
|
|
1596
1598
|
|
|
1597
|
-
private isManagedMailRouteName(routeName: string): boolean {
|
|
1598
|
-
return routeName.startsWith('workapp-mail-');
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
1599
|
private isManagedSmtpUsername(username: string): boolean {
|
|
1602
|
-
return username
|
|
1600
|
+
return isWorkAppManagedSmtpUsername(username);
|
|
1603
1601
|
}
|
|
1604
1602
|
|
|
1605
1603
|
private buildSmtpCredentials(
|
package/ts/email/index.ts
CHANGED
|
@@ -11,5 +11,7 @@ export * from './classes.email-settings.manager.js';
|
|
|
11
11
|
export * from './classes.smartmta-blob-storage-manager.js';
|
|
12
12
|
export * from './classes.smartmta-storage-manager.js';
|
|
13
13
|
export * from './smartmta-storage-policy.js';
|
|
14
|
+
export * from './classes.smtp-account.manager.js';
|
|
15
|
+
export * from './smtp-scram.js';
|
|
14
16
|
export * from './classes.workapp-mail-manager.js';
|
|
15
17
|
export * from './email-dns-records.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { deriveScramVerifier } from '@push.rocks/smartmta';
|
|
3
|
+
import type { ISmtpScramVerifier } from '@push.rocks/smartmta';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive an RFC 5802 SCRAM-SHA-256 verifier from a plaintext password using
|
|
7
|
+
* smartmta's canonical exported derivation (fresh random salt, OWASP-aligned
|
|
8
|
+
* default iteration count) — the exact algorithm the server verifies at AUTH
|
|
9
|
+
* time. Only the verifier is ever persisted; the plaintext is shown once at
|
|
10
|
+
* create/rotate time and then discarded.
|
|
11
|
+
*/
|
|
12
|
+
export function deriveSmtpScramVerifier(
|
|
13
|
+
password: string,
|
|
14
|
+
iterations?: number,
|
|
15
|
+
): ISmtpScramVerifier {
|
|
16
|
+
return deriveScramVerifier(password, iterations);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Machine-generated SMTP account password (~192 bits of entropy). Shown once.
|
|
21
|
+
*/
|
|
22
|
+
export function generateSmtpAccountPassword(): string {
|
|
23
|
+
return plugins.crypto.randomBytes(24).toString('base64url');
|
|
24
|
+
}
|
|
@@ -4,7 +4,15 @@ import { MetricsCache } from './classes.metricscache.js';
|
|
|
4
4
|
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
|
|
5
5
|
import { logger } from '../logger.js';
|
|
6
6
|
import type { IAsnActivity, IProtocolConnectionHistoryPoint } from '../../ts_interfaces/data/stats.js';
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
EmailTrafficBucketDoc,
|
|
9
|
+
type IEmailTrafficBucketSnapshot,
|
|
10
|
+
} from '../db/documents/classes.email-traffic-bucket.doc.js';
|
|
11
|
+
|
|
12
|
+
const EMAIL_TRAFFIC_WINDOW_MINUTES = 24 * 60;
|
|
13
|
+
const EMAIL_TRAFFIC_FLUSH_INTERVAL_MS = 5_000;
|
|
14
|
+
const EMAIL_TRAFFIC_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
15
|
+
const AUTHENTICATION_EVENT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
8
16
|
const NETWORK_STATS_CACHE_TTL_MS = 3_000;
|
|
9
17
|
const ASN_INTELLIGENCE_CACHE_TTL_MS = 60_000;
|
|
10
18
|
const ACTIVE_CONNECTION_SNAPSHOT_CACHE_TTL_MS = 3_000;
|
|
@@ -20,6 +28,12 @@ export class MetricsManager {
|
|
|
20
28
|
private dcRouter: DcRouter;
|
|
21
29
|
private resetInterval?: NodeJS.Timeout;
|
|
22
30
|
private observedIpSamplerInterval?: NodeJS.Timeout;
|
|
31
|
+
private emailTrafficFlushInterval?: NodeJS.Timeout;
|
|
32
|
+
private persistedMetricsPruneInterval?: NodeJS.Timeout;
|
|
33
|
+
private emailTrafficFlushChain: Promise<void> = Promise.resolve();
|
|
34
|
+
private persistedMetricsFlushInFlight?: Promise<void>;
|
|
35
|
+
private persistedMetricsPruneInFlight?: Promise<void>;
|
|
36
|
+
private emailDirtyBuckets = new Map<number, number>();
|
|
23
37
|
private metricsCache: MetricsCache;
|
|
24
38
|
|
|
25
39
|
// Constants
|
|
@@ -32,7 +46,7 @@ export class MetricsManager {
|
|
|
32
46
|
failedToday: 0,
|
|
33
47
|
bouncedToday: 0,
|
|
34
48
|
queueSize: 0,
|
|
35
|
-
lastResetDate: new Date().
|
|
49
|
+
lastResetDate: new Date().toISOString().slice(0, 10),
|
|
36
50
|
deliveryTimes: [] as number[], // Track delivery times in ms
|
|
37
51
|
recipients: new Map<string, number>(), // Track email count by recipient
|
|
38
52
|
recentActivity: [] as Array<{ timestamp: number; type: string; details: string }>,
|
|
@@ -54,7 +68,9 @@ export class MetricsManager {
|
|
|
54
68
|
};
|
|
55
69
|
|
|
56
70
|
// Per-minute time-series buckets for charts
|
|
57
|
-
private emailMinuteBuckets = new Map<number, {
|
|
71
|
+
private emailMinuteBuckets = new Map<number, {
|
|
72
|
+
sent: number; received: number; failed: number; revision: number;
|
|
73
|
+
}>();
|
|
58
74
|
private dnsMinuteBuckets = new Map<number, { queries: number }>();
|
|
59
75
|
|
|
60
76
|
// Track security-specific metrics
|
|
@@ -84,23 +100,18 @@ export class MetricsManager {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
public async start(): Promise<void> {
|
|
103
|
+
if (this.canPersistEmailTraffic()) {
|
|
104
|
+
await this.loadEmailTrafficBuckets();
|
|
105
|
+
await this.prunePersistedMetrics();
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
// Start SmartMetrics collection
|
|
88
109
|
this.smartMetrics.start();
|
|
89
110
|
|
|
90
|
-
//
|
|
111
|
+
// Maintain UTC daily ancillary counters and bounded in-memory windows.
|
|
91
112
|
this.resetInterval = setInterval(() => {
|
|
92
113
|
const currentDate = new Date().toDateString();
|
|
93
|
-
|
|
94
|
-
if (currentDate !== this.emailMetrics.lastResetDate) {
|
|
95
|
-
this.emailMetrics.sentToday = 0;
|
|
96
|
-
this.emailMetrics.receivedToday = 0;
|
|
97
|
-
this.emailMetrics.failedToday = 0;
|
|
98
|
-
this.emailMetrics.bouncedToday = 0;
|
|
99
|
-
this.emailMetrics.deliveryTimes = [];
|
|
100
|
-
this.emailMetrics.recipients.clear();
|
|
101
|
-
this.emailMetrics.recentActivity = [];
|
|
102
|
-
this.emailMetrics.lastResetDate = currentDate;
|
|
103
|
-
}
|
|
114
|
+
this.ensureEmailDailyState();
|
|
104
115
|
|
|
105
116
|
if (currentDate !== this.dnsMetrics.lastResetDate) {
|
|
106
117
|
this.dnsMetrics.totalQueries = 0;
|
|
@@ -128,17 +139,33 @@ export class MetricsManager {
|
|
|
128
139
|
// Prune old time-series buckets every minute (don't wait for lazy query)
|
|
129
140
|
this.pruneOldBuckets();
|
|
130
141
|
}, 60000); // Check every minute
|
|
142
|
+
this.resetInterval.unref();
|
|
131
143
|
|
|
132
144
|
this.observedIpSamplerInterval = setInterval(() => {
|
|
133
145
|
this.sampleObservedIps();
|
|
134
146
|
}, OBSERVED_IP_SAMPLE_INTERVAL_MS);
|
|
135
147
|
this.observedIpSamplerInterval.unref();
|
|
148
|
+
|
|
149
|
+
if (this.canPersistEmailTraffic()) {
|
|
150
|
+
this.emailTrafficFlushInterval = setInterval(() => {
|
|
151
|
+
void this.flushPersistedMetrics().catch((error) => {
|
|
152
|
+
logger.log('warn', `Metrics persistence flush failed: ${(error as Error).message}`);
|
|
153
|
+
});
|
|
154
|
+
}, EMAIL_TRAFFIC_FLUSH_INTERVAL_MS);
|
|
155
|
+
this.emailTrafficFlushInterval.unref();
|
|
156
|
+
|
|
157
|
+
this.persistedMetricsPruneInterval = setInterval(() => {
|
|
158
|
+
void this.prunePersistedMetrics().catch((error) => {
|
|
159
|
+
logger.log('warn', `Metrics persistence pruning failed: ${(error as Error).message}`);
|
|
160
|
+
});
|
|
161
|
+
}, EMAIL_TRAFFIC_PRUNE_INTERVAL_MS);
|
|
162
|
+
this.persistedMetricsPruneInterval.unref();
|
|
163
|
+
}
|
|
136
164
|
|
|
137
165
|
logger.log('info', 'MetricsManager started');
|
|
138
166
|
}
|
|
139
167
|
|
|
140
168
|
public async stop(): Promise<void> {
|
|
141
|
-
// Clear the reset interval
|
|
142
169
|
if (this.resetInterval) {
|
|
143
170
|
clearInterval(this.resetInterval);
|
|
144
171
|
this.resetInterval = undefined;
|
|
@@ -147,12 +174,60 @@ export class MetricsManager {
|
|
|
147
174
|
clearInterval(this.observedIpSamplerInterval);
|
|
148
175
|
this.observedIpSamplerInterval = undefined;
|
|
149
176
|
}
|
|
177
|
+
if (this.emailTrafficFlushInterval) {
|
|
178
|
+
clearInterval(this.emailTrafficFlushInterval);
|
|
179
|
+
this.emailTrafficFlushInterval = undefined;
|
|
180
|
+
}
|
|
181
|
+
if (this.persistedMetricsPruneInterval) {
|
|
182
|
+
clearInterval(this.persistedMetricsPruneInterval);
|
|
183
|
+
this.persistedMetricsPruneInterval = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.persistedMetricsPruneInFlight) {
|
|
187
|
+
try {
|
|
188
|
+
await this.persistedMetricsPruneInFlight;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.log('warn', `Final metrics pruning wait failed: ${(error as Error).message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let finalFlushError: Error | undefined;
|
|
195
|
+
if (this.canPersistEmailTraffic()) {
|
|
196
|
+
const activeFlush = this.persistedMetricsFlushInFlight;
|
|
197
|
+
if (activeFlush) {
|
|
198
|
+
try {
|
|
199
|
+
await activeFlush;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logger.log(
|
|
202
|
+
'warn',
|
|
203
|
+
`Active metrics persistence flush failed before final drain: ${(error as Error).message}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// OpsServer and EmailServer have stopped by this point. Run a fresh,
|
|
209
|
+
// non-coalesced pass so events added after an earlier flush snapshot are
|
|
210
|
+
// included in the final durable drain.
|
|
211
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
212
|
+
try {
|
|
213
|
+
await this.flushPersistedMetricsUnlocked();
|
|
214
|
+
finalFlushError = undefined;
|
|
215
|
+
break;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
finalFlushError = error as Error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
150
221
|
|
|
151
222
|
this.smartMetrics.stop();
|
|
152
223
|
|
|
153
|
-
|
|
224
|
+
if (finalFlushError) {
|
|
225
|
+
throw new Error(`Final metrics persistence flush failed: ${finalFlushError.message}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
154
228
|
this.metricsCache.clear();
|
|
155
229
|
this.emailMinuteBuckets.clear();
|
|
230
|
+
this.emailDirtyBuckets.clear();
|
|
156
231
|
this.dnsMinuteBuckets.clear();
|
|
157
232
|
|
|
158
233
|
logger.log('info', 'MetricsManager stopped');
|
|
@@ -199,30 +274,31 @@ export class MetricsManager {
|
|
|
199
274
|
// Get email metrics
|
|
200
275
|
public async getEmailStats() {
|
|
201
276
|
return this.metricsCache.get('emailStats', () => {
|
|
202
|
-
|
|
277
|
+
this.ensureEmailDailyState();
|
|
278
|
+
const totals = this.getUtcTodayEmailTotals();
|
|
279
|
+
const deliveryAttempts = totals.sent + totals.failed;
|
|
203
280
|
const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
|
|
204
281
|
? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
|
|
205
282
|
: 0;
|
|
206
|
-
|
|
207
|
-
// Get top recipients
|
|
283
|
+
|
|
208
284
|
const topRecipients = Array.from(this.emailMetrics.recipients.entries())
|
|
209
285
|
.sort((a, b) => b[1] - a[1])
|
|
210
286
|
.slice(0, 10)
|
|
211
287
|
.map(([email, count]) => ({ email, count }));
|
|
212
|
-
|
|
213
|
-
// Get recent activity (last 50 entries)
|
|
288
|
+
|
|
214
289
|
const recentActivity = this.emailMetrics.recentActivity.slice(-50);
|
|
215
|
-
|
|
290
|
+
|
|
216
291
|
return {
|
|
217
|
-
sentToday:
|
|
218
|
-
receivedToday:
|
|
219
|
-
failedToday:
|
|
220
|
-
|
|
221
|
-
|
|
292
|
+
sentToday: totals.sent,
|
|
293
|
+
receivedToday: totals.received,
|
|
294
|
+
failedToday: totals.failed,
|
|
295
|
+
bouncedToday: this.emailMetrics.bouncedToday,
|
|
296
|
+
bounceRate: deliveryAttempts > 0
|
|
297
|
+
? Math.min(1, Math.max(0, this.emailMetrics.bouncedToday / deliveryAttempts))
|
|
222
298
|
: 0,
|
|
223
|
-
deliveryRate:
|
|
224
|
-
? ((
|
|
225
|
-
:
|
|
299
|
+
deliveryRate: deliveryAttempts > 0
|
|
300
|
+
? Math.min(1, Math.max(0, totals.sent / deliveryAttempts))
|
|
301
|
+
: 1,
|
|
226
302
|
queueSize: this.emailMetrics.queueSize,
|
|
227
303
|
averageDeliveryTime: Math.round(avgDeliveryTime),
|
|
228
304
|
topRecipients,
|
|
@@ -278,8 +354,13 @@ export class MetricsManager {
|
|
|
278
354
|
this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
|
|
279
355
|
this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
|
|
280
356
|
this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
|
|
281
|
-
this.securityMetrics.authFailures =
|
|
282
|
-
|
|
357
|
+
this.securityMetrics.authFailures = securityLogger
|
|
358
|
+
.getRecentEvents(10_000, {
|
|
359
|
+
type: SecurityEventType.AUTHENTICATION,
|
|
360
|
+
fromTimestamp: Date.now() - 86_400_000,
|
|
361
|
+
})
|
|
362
|
+
.filter((event) => event.success === false)
|
|
363
|
+
.length;
|
|
283
364
|
this.securityMetrics.blockedIPs =
|
|
284
365
|
(summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
|
|
285
366
|
(summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
|
|
@@ -377,7 +458,7 @@ export class MetricsManager {
|
|
|
377
458
|
|
|
378
459
|
// Email event tracking methods
|
|
379
460
|
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
|
380
|
-
this.
|
|
461
|
+
this.ensureEmailDailyState();
|
|
381
462
|
this.incrementEmailBucket('sent');
|
|
382
463
|
|
|
383
464
|
if (recipient) {
|
|
@@ -414,7 +495,7 @@ export class MetricsManager {
|
|
|
414
495
|
}
|
|
415
496
|
|
|
416
497
|
public trackEmailReceived(sender?: string): void {
|
|
417
|
-
this.
|
|
498
|
+
this.ensureEmailDailyState();
|
|
418
499
|
this.incrementEmailBucket('received');
|
|
419
500
|
|
|
420
501
|
this.emailMetrics.recentActivity.push({
|
|
@@ -430,7 +511,7 @@ export class MetricsManager {
|
|
|
430
511
|
}
|
|
431
512
|
|
|
432
513
|
public trackEmailFailed(recipient?: string, reason?: string): void {
|
|
433
|
-
this.
|
|
514
|
+
this.ensureEmailDailyState();
|
|
434
515
|
this.incrementEmailBucket('failed');
|
|
435
516
|
|
|
436
517
|
this.emailMetrics.recentActivity.push({
|
|
@@ -446,6 +527,7 @@ export class MetricsManager {
|
|
|
446
527
|
}
|
|
447
528
|
|
|
448
529
|
public trackEmailBounced(recipient?: string): void {
|
|
530
|
+
this.ensureEmailDailyState();
|
|
449
531
|
this.emailMetrics.bouncedToday++;
|
|
450
532
|
|
|
451
533
|
this.emailMetrics.recentActivity.push({
|
|
@@ -1087,14 +1169,164 @@ export class MetricsManager {
|
|
|
1087
1169
|
return Math.floor(ts / 60000) * 60000;
|
|
1088
1170
|
}
|
|
1089
1171
|
|
|
1172
|
+
private static firstMinuteForHours(hoursArg: number, nowArg = Date.now()): number {
|
|
1173
|
+
const pointCount = Math.max(1, Math.floor(hoursArg * 60));
|
|
1174
|
+
return MetricsManager.minuteKey(nowArg) - (pointCount - 1) * 60_000;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private isEmailTrafficPersistenceConfigured(): boolean {
|
|
1178
|
+
return this.dcRouter.options.dbConfig?.enabled !== false;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private canPersistEmailTraffic(): boolean {
|
|
1182
|
+
return this.isEmailTrafficPersistenceConfigured()
|
|
1183
|
+
&& Boolean(this.dcRouter.dcRouterDb?.isReady());
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private ensureEmailDailyState(nowArg = Date.now()): void {
|
|
1187
|
+
const currentUtcDate = new Date(nowArg).toISOString().slice(0, 10);
|
|
1188
|
+
if (currentUtcDate === this.emailMetrics.lastResetDate) return;
|
|
1189
|
+
|
|
1190
|
+
this.emailMetrics.sentToday = 0;
|
|
1191
|
+
this.emailMetrics.receivedToday = 0;
|
|
1192
|
+
this.emailMetrics.failedToday = 0;
|
|
1193
|
+
this.emailMetrics.bouncedToday = 0;
|
|
1194
|
+
this.emailMetrics.deliveryTimes = [];
|
|
1195
|
+
this.emailMetrics.recipients.clear();
|
|
1196
|
+
this.emailMetrics.recentActivity = [];
|
|
1197
|
+
this.emailMetrics.lastResetDate = currentUtcDate;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private getUtcTodayEmailTotals(nowArg = Date.now()): {
|
|
1201
|
+
sent: number;
|
|
1202
|
+
received: number;
|
|
1203
|
+
failed: number;
|
|
1204
|
+
} {
|
|
1205
|
+
const now = new Date(nowArg);
|
|
1206
|
+
const utcMidnight = Date.UTC(
|
|
1207
|
+
now.getUTCFullYear(),
|
|
1208
|
+
now.getUTCMonth(),
|
|
1209
|
+
now.getUTCDate(),
|
|
1210
|
+
);
|
|
1211
|
+
const totals = { sent: 0, received: 0, failed: 0 };
|
|
1212
|
+
for (const [bucketStart, bucket] of this.emailMinuteBuckets) {
|
|
1213
|
+
if (bucketStart < utcMidnight || bucketStart > nowArg) continue;
|
|
1214
|
+
totals.sent += bucket.sent;
|
|
1215
|
+
totals.received += bucket.received;
|
|
1216
|
+
totals.failed += bucket.failed;
|
|
1217
|
+
}
|
|
1218
|
+
return totals;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private async loadEmailTrafficBuckets(): Promise<void> {
|
|
1222
|
+
const snapshots = await EmailTrafficBucketDoc.loadSince(
|
|
1223
|
+
MetricsManager.firstMinuteForHours(24),
|
|
1224
|
+
);
|
|
1225
|
+
for (const snapshot of snapshots) {
|
|
1226
|
+
this.emailMinuteBuckets.set(snapshot.bucketStart, {
|
|
1227
|
+
sent: snapshot.sent,
|
|
1228
|
+
received: snapshot.received,
|
|
1229
|
+
failed: snapshot.failed,
|
|
1230
|
+
revision: 0,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private flushPersistedMetrics(): Promise<void> {
|
|
1236
|
+
if (this.persistedMetricsFlushInFlight) return this.persistedMetricsFlushInFlight;
|
|
1237
|
+
const run = this.flushPersistedMetricsUnlocked();
|
|
1238
|
+
this.persistedMetricsFlushInFlight = run;
|
|
1239
|
+
run.then(
|
|
1240
|
+
() => { if (this.persistedMetricsFlushInFlight === run) this.persistedMetricsFlushInFlight = undefined; },
|
|
1241
|
+
() => { if (this.persistedMetricsFlushInFlight === run) this.persistedMetricsFlushInFlight = undefined; },
|
|
1242
|
+
);
|
|
1243
|
+
return run;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private async flushPersistedMetricsUnlocked(): Promise<void> {
|
|
1247
|
+
const results = await Promise.allSettled([
|
|
1248
|
+
this.flushEmailTrafficBuckets(),
|
|
1249
|
+
this.dcRouter.authenticationEventManager.flushBuffered(),
|
|
1250
|
+
]);
|
|
1251
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
1252
|
+
if (failure?.status === 'rejected') {
|
|
1253
|
+
throw failure.reason;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private flushEmailTrafficBuckets(): Promise<void> {
|
|
1258
|
+
const run = this.emailTrafficFlushChain.then(
|
|
1259
|
+
() => this.flushEmailTrafficBucketsUnlocked(),
|
|
1260
|
+
() => this.flushEmailTrafficBucketsUnlocked(),
|
|
1261
|
+
);
|
|
1262
|
+
this.emailTrafficFlushChain = run.then(() => undefined, () => undefined);
|
|
1263
|
+
return run;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private async flushEmailTrafficBucketsUnlocked(): Promise<void> {
|
|
1267
|
+
if (!this.canPersistEmailTraffic() || this.emailDirtyBuckets.size === 0) return;
|
|
1268
|
+
|
|
1269
|
+
const snapshots: Array<IEmailTrafficBucketSnapshot & { revision: number }> = [];
|
|
1270
|
+
for (const [bucketStart, revision] of this.emailDirtyBuckets) {
|
|
1271
|
+
const bucket = this.emailMinuteBuckets.get(bucketStart);
|
|
1272
|
+
if (!bucket) continue;
|
|
1273
|
+
snapshots.push({
|
|
1274
|
+
bucketStart,
|
|
1275
|
+
sent: bucket.sent,
|
|
1276
|
+
received: bucket.received,
|
|
1277
|
+
failed: bucket.failed,
|
|
1278
|
+
revision,
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
for (let offset = 0; offset < snapshots.length; offset += 500) {
|
|
1283
|
+
const batch = snapshots.slice(offset, offset + 500);
|
|
1284
|
+
await EmailTrafficBucketDoc.persistAbsolute(batch);
|
|
1285
|
+
for (const snapshot of batch) {
|
|
1286
|
+
const current = this.emailMinuteBuckets.get(snapshot.bucketStart);
|
|
1287
|
+
if (
|
|
1288
|
+
current?.revision === snapshot.revision
|
|
1289
|
+
&& this.emailDirtyBuckets.get(snapshot.bucketStart) === snapshot.revision
|
|
1290
|
+
) {
|
|
1291
|
+
this.emailDirtyBuckets.delete(snapshot.bucketStart);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private prunePersistedMetrics(): Promise<void> {
|
|
1298
|
+
if (this.persistedMetricsPruneInFlight) return this.persistedMetricsPruneInFlight;
|
|
1299
|
+
const run = this.prunePersistedMetricsUnlocked();
|
|
1300
|
+
this.persistedMetricsPruneInFlight = run;
|
|
1301
|
+
run.then(
|
|
1302
|
+
() => { if (this.persistedMetricsPruneInFlight === run) this.persistedMetricsPruneInFlight = undefined; },
|
|
1303
|
+
() => { if (this.persistedMetricsPruneInFlight === run) this.persistedMetricsPruneInFlight = undefined; },
|
|
1304
|
+
);
|
|
1305
|
+
return run;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
private async prunePersistedMetricsUnlocked(): Promise<void> {
|
|
1309
|
+
if (!this.canPersistEmailTraffic()) return;
|
|
1310
|
+
await Promise.all([
|
|
1311
|
+
EmailTrafficBucketDoc.pruneBefore(MetricsManager.firstMinuteForHours(24)),
|
|
1312
|
+
this.dcRouter.authenticationEventManager.pruneBefore(
|
|
1313
|
+
Date.now() - AUTHENTICATION_EVENT_RETENTION_MS,
|
|
1314
|
+
),
|
|
1315
|
+
]);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1090
1318
|
private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
|
|
1091
1319
|
const key = MetricsManager.minuteKey();
|
|
1092
1320
|
let bucket = this.emailMinuteBuckets.get(key);
|
|
1093
1321
|
if (!bucket) {
|
|
1094
|
-
bucket = { sent: 0, received: 0, failed: 0 };
|
|
1322
|
+
bucket = { sent: 0, received: 0, failed: 0, revision: 0 };
|
|
1095
1323
|
this.emailMinuteBuckets.set(key, bucket);
|
|
1096
1324
|
}
|
|
1097
1325
|
bucket[field]++;
|
|
1326
|
+
bucket.revision++;
|
|
1327
|
+
if (this.isEmailTrafficPersistenceConfigured()) {
|
|
1328
|
+
this.emailDirtyBuckets.set(key, bucket.revision);
|
|
1329
|
+
}
|
|
1098
1330
|
}
|
|
1099
1331
|
|
|
1100
1332
|
private incrementDnsBucket(): void {
|
|
@@ -1165,12 +1397,16 @@ export class MetricsManager {
|
|
|
1165
1397
|
}
|
|
1166
1398
|
|
|
1167
1399
|
private pruneOldBuckets(): void {
|
|
1168
|
-
const
|
|
1400
|
+
const emailCutoff = MetricsManager.firstMinuteForHours(24);
|
|
1169
1401
|
for (const key of this.emailMinuteBuckets.keys()) {
|
|
1170
|
-
if (key <
|
|
1402
|
+
if (key < emailCutoff) {
|
|
1403
|
+
this.emailMinuteBuckets.delete(key);
|
|
1404
|
+
this.emailDirtyBuckets.delete(key);
|
|
1405
|
+
}
|
|
1171
1406
|
}
|
|
1407
|
+
const dnsCutoff = Date.now() - 86400000;
|
|
1172
1408
|
for (const key of this.dnsMinuteBuckets.keys()) {
|
|
1173
|
-
if (key <
|
|
1409
|
+
if (key < dnsCutoff) this.dnsMinuteBuckets.delete(key);
|
|
1174
1410
|
}
|
|
1175
1411
|
}
|
|
1176
1412
|
|
|
@@ -1183,20 +1419,21 @@ export class MetricsManager {
|
|
|
1183
1419
|
failed: Array<{ timestamp: number; value: number }>;
|
|
1184
1420
|
} {
|
|
1185
1421
|
this.pruneOldBuckets();
|
|
1186
|
-
const
|
|
1422
|
+
const pointCount = Math.max(
|
|
1423
|
+
1,
|
|
1424
|
+
Math.min(EMAIL_TRAFFIC_WINDOW_MINUTES, Math.floor(hours * 60)),
|
|
1425
|
+
);
|
|
1426
|
+
const end = MetricsManager.minuteKey();
|
|
1427
|
+
const start = end - (pointCount - 1) * 60_000;
|
|
1187
1428
|
const sent: Array<{ timestamp: number; value: number }> = [];
|
|
1188
1429
|
const received: Array<{ timestamp: number; value: number }> = [];
|
|
1189
1430
|
const failed: Array<{ timestamp: number; value: number }> = [];
|
|
1190
1431
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const bucket = this.emailMinuteBuckets.get(key)!;
|
|
1197
|
-
sent.push({ timestamp: key, value: bucket.sent });
|
|
1198
|
-
received.push({ timestamp: key, value: bucket.received });
|
|
1199
|
-
failed.push({ timestamp: key, value: bucket.failed });
|
|
1432
|
+
for (let timestamp = start; timestamp <= end; timestamp += 60_000) {
|
|
1433
|
+
const bucket = this.emailMinuteBuckets.get(timestamp);
|
|
1434
|
+
sent.push({ timestamp, value: bucket?.sent || 0 });
|
|
1435
|
+
received.push({ timestamp, value: bucket?.received || 0 });
|
|
1436
|
+
failed.push({ timestamp, value: bucket?.failed || 0 });
|
|
1200
1437
|
}
|
|
1201
1438
|
|
|
1202
1439
|
return { sent, received, failed };
|
|
@@ -41,6 +41,7 @@ export class OpsServer {
|
|
|
41
41
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
|
42
42
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
43
43
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
44
|
+
private smtpAccountHandler!: handlers.SmtpAccountHandler;
|
|
44
45
|
private gatewayClientHandler!: handlers.GatewayClientHandler;
|
|
45
46
|
private webPushControlHandler!: handlers.WebPushControlHandler;
|
|
46
47
|
private webPushAppHandler!: handlers.WebPushAppHandler;
|
|
@@ -109,6 +110,7 @@ export class OpsServer {
|
|
|
109
110
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
|
110
111
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
111
112
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
113
|
+
this.smtpAccountHandler = new handlers.SmtpAccountHandler(this);
|
|
112
114
|
this.gatewayClientHandler = new handlers.GatewayClientHandler(this);
|
|
113
115
|
this.webPushControlHandler = new handlers.WebPushControlHandler(this);
|
|
114
116
|
this.webPushAppHandler = new handlers.WebPushAppHandler(this);
|