@serve.zone/dcrouter 18.9.3 → 18.9.5

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 (42) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +5 -4
  4. package/dist_ts/db/documents/classes.authentication-event.doc.d.ts +0 -1
  5. package/dist_ts/db/documents/classes.authentication-event.doc.js +176 -259
  6. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +110 -188
  7. package/dist_ts/dns/manager.dns-authority.d.ts +5 -0
  8. package/dist_ts/dns/manager.dns-authority.js +97 -53
  9. package/dist_ts/dns/manager.dns.js +23 -5
  10. package/dist_ts/email/classes.email-domain.manager.js +14 -3
  11. package/dist_ts/email/classes.email-settings.manager.js +26 -6
  12. package/dist_ts/email/classes.mail-dns-sync.js +43 -11
  13. package/dist_ts/monitoring/classes.metricscache.d.ts +28 -19
  14. package/dist_ts/monitoring/classes.metricscache.js +133 -62
  15. package/dist_ts/monitoring/classes.metricsmanager.js +7 -5
  16. package/dist_ts/plugins.d.ts +2 -1
  17. package/dist_ts/plugins.js +3 -2
  18. package/dist_ts/remoteingress/classes.remoteingress-manager.js +46 -9
  19. package/dist_ts/security/classes.authentication-event-manager.d.ts +5 -1
  20. package/dist_ts/security/classes.authentication-event-manager.js +32 -22
  21. package/dist_ts/vpn/classes.vpn-manager.js +34 -2
  22. package/dist_ts_migrations/index.d.ts +4 -1
  23. package/dist_ts_migrations/index.js +18 -3
  24. package/dist_ts_web/00_commitinfo_data.js +1 -1
  25. package/package.json +7 -6
  26. package/readme.hints.md +84 -0
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.dcrouter.ts +4 -3
  29. package/ts/db/documents/classes.authentication-event.doc.ts +64 -87
  30. package/ts/db/documents/classes.email-traffic-bucket.doc.ts +10 -9
  31. package/ts/dns/manager.dns-authority.ts +92 -54
  32. package/ts/dns/manager.dns.ts +20 -4
  33. package/ts/email/classes.email-domain.manager.ts +12 -2
  34. package/ts/email/classes.email-settings.manager.ts +23 -5
  35. package/ts/email/classes.mail-dns-sync.ts +45 -10
  36. package/ts/monitoring/classes.metricscache.ts +133 -79
  37. package/ts/monitoring/classes.metricsmanager.ts +6 -4
  38. package/ts/plugins.ts +2 -0
  39. package/ts/remoteingress/classes.remoteingress-manager.ts +42 -8
  40. package/ts/security/classes.authentication-event-manager.ts +31 -20
  41. package/ts/vpn/classes.vpn-manager.ts +32 -1
  42. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import type { DcRouter } from '../classes.dcrouter.js';
4
+ import { MetricsCache } from '../monitoring/classes.metricscache.js';
4
5
  import { AuthenticationEventDoc } from '../db/documents/classes.authentication-event.doc.js';
5
6
  import type {
6
7
  IAuthenticationEvent,
@@ -29,16 +30,23 @@ export class AuthenticationEventManager {
29
30
  private recentEvents = new Map<string, IAuthenticationEvent>();
30
31
  private flushInFlight?: Promise<void>;
31
32
  private pruneInFlight?: Promise<number>;
32
- private windowStatsInFlight = new Map<string, Promise<IAuthenticationWindowStats>>();
33
+ private windowStatsCache = new MetricsCache(1_000, { maxAggregateEntries: 8, maxInFlight: 8 });
34
+ private lastPersistenceAvailable?: boolean;
33
35
 
34
36
  constructor(private dcRouterRef: DcRouter) {}
35
37
 
38
+ public start(): void { this.windowStatsCache.start(); }
39
+
40
+ /** Drain observer reads; MetricsManager still owns the final persistence flush. */
41
+ public async stop(): Promise<void> { await this.windowStatsCache.stop(); }
42
+
36
43
  private canPersist(): boolean {
37
44
  return this.dcRouterRef.options.dbConfig?.enabled !== false
38
45
  && Boolean(this.dcRouterRef.dcRouterDb?.isReady());
39
46
  }
40
47
 
41
48
  private rememberEvent(eventArg: IAuthenticationEvent): void {
49
+ this.windowStatsCache.clear();
42
50
  this.pendingEvents.set(eventArg.id, eventArg);
43
51
  this.recentEvents.set(eventArg.id, eventArg);
44
52
  while (this.pendingEvents.size > MAX_IN_MEMORY_EVENTS) {
@@ -108,7 +116,11 @@ export class AuthenticationEventManager {
108
116
 
109
117
  public flushBuffered(): Promise<void> {
110
118
  if (this.flushInFlight) return this.flushInFlight;
111
- const run = this.flushBufferedUnlocked();
119
+ const mutates = this.canPersist() && this.pendingEvents.size > 0;
120
+ if (mutates) this.windowStatsCache.clear();
121
+ const run = this.flushBufferedUnlocked().finally(() => {
122
+ if (mutates) this.windowStatsCache.clear();
123
+ });
112
124
  this.flushInFlight = run;
113
125
  run.then(
114
126
  () => { if (this.flushInFlight === run) this.flushInFlight = undefined; },
@@ -119,9 +131,10 @@ export class AuthenticationEventManager {
119
131
 
120
132
  public pruneBefore(cutoffArg: number): Promise<number> {
121
133
  if (this.pruneInFlight) return this.pruneInFlight;
122
- const run = this.canPersist()
134
+ this.windowStatsCache.clear();
135
+ const run = (this.canPersist()
123
136
  ? AuthenticationEventDoc.pruneBefore(cutoffArg)
124
- : Promise.resolve(0);
137
+ : Promise.resolve(0)).finally(() => { this.windowStatsCache.clear(); });
125
138
  this.pruneInFlight = run;
126
139
  run.then(
127
140
  () => { if (this.pruneInFlight === run) this.pruneInFlight = undefined; },
@@ -151,12 +164,6 @@ export class AuthenticationEventManager {
151
164
  return this.getInMemoryWindow(cutoff, limitArg);
152
165
  }
153
166
 
154
- try {
155
- await this.flushBuffered();
156
- } catch (error) {
157
- logger.log('warn', `Unable to flush authentication event buffer: ${(error as Error).message}`);
158
- }
159
-
160
167
  try {
161
168
  const durable = await AuthenticationEventDoc.getWindowSummary(cutoff, limitArg);
162
169
  const buffered = [...this.pendingEvents.values()]
@@ -190,7 +197,7 @@ export class AuthenticationEventManager {
190
197
  }
191
198
  }
192
199
 
193
- public getWindowStats(
200
+ public async getWindowStats(
194
201
  windowMsArg = 24 * 60 * 60 * 1000,
195
202
  limitArg = 100,
196
203
  ): Promise<IAuthenticationWindowStats> {
@@ -200,21 +207,25 @@ export class AuthenticationEventManager {
200
207
  if (!Number.isSafeInteger(limitArg) || limitArg <= 0 || limitArg > 500) {
201
208
  return Promise.reject(new Error('Authentication event limit must be from 1 to 500'));
202
209
  }
203
- const key = `${windowMsArg}:${limitArg}`;
204
- const existing = this.windowStatsInFlight.get(key);
205
- if (existing) return existing;
206
- const run = this.getWindowStatsUnlocked(windowMsArg, limitArg);
207
- this.windowStatsInFlight.set(key, run);
208
- run.then(
209
- () => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
210
- () => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
210
+ const persistenceAvailable = this.canPersist();
211
+ if (this.lastPersistenceAvailable !== persistenceAvailable) this.windowStatsCache.clear();
212
+ this.lastPersistenceAvailable = persistenceAvailable;
213
+ // Drain before entering the shared read boundary. Recording authentication itself never waits here.
214
+ try {
215
+ await this.flushBuffered();
216
+ } catch (error) {
217
+ logger.log('warn', `Unable to flush authentication event buffer: ${(error as Error).message}`);
218
+ }
219
+ return await this.windowStatsCache.get(
220
+ `${windowMsArg}:${limitArg}`,
221
+ () => this.getWindowStatsUnlocked(windowMsArg, limitArg),
211
222
  );
212
- return run;
213
223
  }
214
224
 
215
225
  /** Test-only reset for the DcRouter-owned in-process buffers. */
216
226
  public async resetForTests(): Promise<void> {
217
227
  await this.flushInFlight?.catch(() => undefined);
228
+ this.windowStatsCache.clear();
218
229
  this.pendingEvents.clear();
219
230
  this.recentEvents.clear();
220
231
  }
@@ -811,6 +811,8 @@ export class VpnManager {
811
811
  client: Pick<VpnClientDoc, 'useHostIp' | 'useDhcp' | 'staticIp' | 'forceVlan' | 'vlanId'>,
812
812
  ): void {
813
813
  client.useHostIp = client.useHostIp === true;
814
+ client.staticIp = client.staticIp ?? undefined;
815
+ client.vlanId = client.vlanId ?? undefined;
814
816
 
815
817
  if (!client.useHostIp) {
816
818
  client.useDhcp = false;
@@ -883,6 +885,35 @@ export class VpnManager {
883
885
  }
884
886
 
885
887
  private async persistClient(client: VpnClientDoc): Promise<void> {
886
- await client.save();
888
+ if (client.creationStatus === 'new') {
889
+ await client.save();
890
+ return;
891
+ }
892
+ const persisted = await VpnClientDoc.atomicUpdate({ clientId: client.clientId }, {
893
+ $set: {
894
+ enabled: client.enabled,
895
+ noisePublicKey: client.noisePublicKey,
896
+ wgPublicKey: client.wgPublicKey,
897
+ createdAt: client.createdAt,
898
+ updatedAt: client.updatedAt,
899
+ ...(client.targetProfileIds !== undefined ? { targetProfileIds: client.targetProfileIds } : {}),
900
+ ...(client.description !== undefined ? { description: client.description } : {}),
901
+ ...(client.assignedIp !== undefined ? { assignedIp: client.assignedIp } : {}),
902
+ ...(client.wgPrivateKey !== undefined ? { wgPrivateKey: client.wgPrivateKey } : {}),
903
+ ...(client.expiresAt !== undefined ? { expiresAt: client.expiresAt } : {}),
904
+ ...(client.destinationAllowList !== undefined ? { destinationAllowList: client.destinationAllowList } : {}),
905
+ ...(client.destinationBlockList !== undefined ? { destinationBlockList: client.destinationBlockList } : {}),
906
+ ...(client.useHostIp !== undefined ? { useHostIp: client.useHostIp } : {}),
907
+ ...(client.useDhcp !== undefined ? { useDhcp: client.useDhcp } : {}),
908
+ ...(client.staticIp !== undefined ? { staticIp: client.staticIp } : {}),
909
+ ...(client.forceVlan !== undefined ? { forceVlan: client.forceVlan } : {}),
910
+ ...(client.vlanId !== undefined ? { vlanId: client.vlanId } : {}),
911
+ },
912
+ $unset: {
913
+ ...(client.staticIp === undefined ? { staticIp: true } : {}),
914
+ ...(client.vlanId === undefined ? { vlanId: true } : {}),
915
+ },
916
+ });
917
+ if (persisted.matchedCount !== 1) throw new Error('VPN client disappeared during update');
887
918
  }
888
919
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.9.3',
6
+ version: '18.9.5',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }