@serve.zone/dcrouter 11.23.5 → 12.1.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.
Files changed (128) hide show
  1. package/dist_serve/bundle.js +1052 -939
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/classes.cert-provision-scheduler.d.ts +6 -8
  4. package/dist_ts/classes.cert-provision-scheduler.js +37 -17
  5. package/dist_ts/classes.dcrouter.d.ts +26 -29
  6. package/dist_ts/classes.dcrouter.js +101 -91
  7. package/dist_ts/classes.storage-cert-manager.d.ts +3 -6
  8. package/dist_ts/classes.storage-cert-manager.js +35 -25
  9. package/dist_ts/config/classes.api-token-manager.d.ts +1 -3
  10. package/dist_ts/config/classes.api-token-manager.js +45 -15
  11. package/dist_ts/config/classes.route-config-manager.d.ts +1 -3
  12. package/dist_ts/config/classes.route-config-manager.js +62 -24
  13. package/dist_ts/{cache → db}/classes.cache.cleaner.d.ts +3 -3
  14. package/dist_ts/db/classes.cache.cleaner.js +130 -0
  15. package/dist_ts/{cache → db}/classes.cached.document.js +1 -1
  16. package/dist_ts/db/classes.dcrouter-db.d.ts +70 -0
  17. package/dist_ts/db/classes.dcrouter-db.js +146 -0
  18. package/dist_ts/db/documents/classes.accounting-session.doc.d.ts +32 -0
  19. package/dist_ts/db/documents/classes.accounting-session.doc.js +214 -0
  20. package/dist_ts/db/documents/classes.acme-cert.doc.d.ts +13 -0
  21. package/dist_ts/db/documents/classes.acme-cert.doc.js +109 -0
  22. package/dist_ts/db/documents/classes.api-token.doc.d.ts +18 -0
  23. package/dist_ts/db/documents/classes.api-token.doc.js +127 -0
  24. package/dist_ts/{cache → db}/documents/classes.cached.email.js +3 -3
  25. package/dist_ts/{cache → db}/documents/classes.cached.ip.reputation.js +3 -3
  26. package/dist_ts/db/documents/classes.cert-backoff.doc.d.ts +11 -0
  27. package/dist_ts/db/documents/classes.cert-backoff.doc.js +97 -0
  28. package/dist_ts/db/documents/classes.proxy-cert.doc.d.ts +12 -0
  29. package/dist_ts/db/documents/classes.proxy-cert.doc.js +103 -0
  30. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +17 -0
  31. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +130 -0
  32. package/dist_ts/db/documents/classes.route-override.doc.d.ts +10 -0
  33. package/dist_ts/db/documents/classes.route-override.doc.js +91 -0
  34. package/dist_ts/db/documents/classes.stored-route.doc.d.ts +12 -0
  35. package/dist_ts/db/documents/classes.stored-route.doc.js +103 -0
  36. package/dist_ts/db/documents/classes.vlan-mappings.doc.d.ts +15 -0
  37. package/dist_ts/db/documents/classes.vlan-mappings.doc.js +77 -0
  38. package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +26 -0
  39. package/dist_ts/db/documents/classes.vpn-client.doc.js +184 -0
  40. package/dist_ts/db/documents/classes.vpn-server-keys.doc.d.ts +10 -0
  41. package/dist_ts/db/documents/classes.vpn-server-keys.doc.js +94 -0
  42. package/dist_ts/db/documents/index.d.ts +13 -0
  43. package/dist_ts/db/documents/index.js +20 -0
  44. package/dist_ts/{cache → db}/index.d.ts +1 -1
  45. package/dist_ts/db/index.js +9 -0
  46. package/dist_ts/opsserver/handlers/certificate.handler.js +66 -66
  47. package/dist_ts/opsserver/handlers/config.handler.js +14 -15
  48. package/dist_ts/opsserver/handlers/vpn.handler.js +35 -1
  49. package/dist_ts/paths.d.ts +0 -1
  50. package/dist_ts/paths.js +1 -2
  51. package/dist_ts/radius/classes.accounting.manager.d.ts +4 -12
  52. package/dist_ts/radius/classes.accounting.manager.js +80 -93
  53. package/dist_ts/radius/classes.radius.server.d.ts +1 -3
  54. package/dist_ts/radius/classes.radius.server.js +4 -6
  55. package/dist_ts/radius/classes.vlan.manager.d.ts +3 -7
  56. package/dist_ts/radius/classes.vlan.manager.js +21 -28
  57. package/dist_ts/radius/index.d.ts +1 -1
  58. package/dist_ts/radius/index.js +1 -1
  59. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +3 -5
  60. package/dist_ts/remoteingress/classes.remoteingress-manager.js +41 -21
  61. package/dist_ts/security/classes.ipreputationchecker.d.ts +6 -21
  62. package/dist_ts/security/classes.ipreputationchecker.js +59 -138
  63. package/dist_ts/vpn/classes.vpn-manager.d.ts +37 -22
  64. package/dist_ts/vpn/classes.vpn-manager.js +161 -51
  65. package/dist_ts_interfaces/data/vpn.d.ts +8 -0
  66. package/dist_ts_interfaces/requests/vpn.d.ts +16 -0
  67. package/dist_ts_oci_container/index.js +4 -4
  68. package/dist_ts_web/00_commitinfo_data.js +2 -2
  69. package/dist_ts_web/appstate.d.ts +16 -0
  70. package/dist_ts_web/appstate.js +17 -1
  71. package/dist_ts_web/elements/ops-view-vpn.js +155 -3
  72. package/package.json +3 -3
  73. package/readme.storage.md +55 -91
  74. package/ts/00_commitinfo_data.ts +1 -1
  75. package/ts/classes.cert-provision-scheduler.ts +35 -17
  76. package/ts/classes.dcrouter.ts +129 -125
  77. package/ts/classes.storage-cert-manager.ts +34 -22
  78. package/ts/config/classes.api-token-manager.ts +42 -11
  79. package/ts/config/classes.route-config-manager.ts +56 -21
  80. package/ts/{cache → db}/classes.cache.cleaner.ts +6 -6
  81. package/ts/db/classes.dcrouter-db.ts +179 -0
  82. package/ts/db/documents/classes.accounting-session.doc.ts +106 -0
  83. package/ts/db/documents/classes.acme-cert.doc.ts +41 -0
  84. package/ts/db/documents/classes.api-token.doc.ts +56 -0
  85. package/ts/{cache → db}/documents/classes.cached.email.ts +2 -2
  86. package/ts/{cache → db}/documents/classes.cached.ip.reputation.ts +2 -2
  87. package/ts/db/documents/classes.cert-backoff.doc.ts +35 -0
  88. package/ts/db/documents/classes.proxy-cert.doc.ts +38 -0
  89. package/ts/db/documents/classes.remote-ingress-edge.doc.ts +54 -0
  90. package/ts/db/documents/classes.route-override.doc.ts +32 -0
  91. package/ts/db/documents/classes.stored-route.doc.ts +38 -0
  92. package/ts/db/documents/classes.vlan-mappings.doc.ts +32 -0
  93. package/ts/db/documents/classes.vpn-client.doc.ts +81 -0
  94. package/ts/db/documents/classes.vpn-server-keys.doc.ts +31 -0
  95. package/ts/db/documents/index.ts +24 -0
  96. package/ts/{cache → db}/index.ts +6 -2
  97. package/ts/opsserver/handlers/certificate.handler.ts +67 -65
  98. package/ts/opsserver/handlers/config.handler.ts +13 -14
  99. package/ts/opsserver/handlers/vpn.handler.ts +37 -0
  100. package/ts/paths.ts +0 -1
  101. package/ts/radius/classes.accounting.manager.ts +81 -103
  102. package/ts/radius/classes.radius.server.ts +3 -6
  103. package/ts/radius/classes.vlan.manager.ts +20 -32
  104. package/ts/radius/index.ts +1 -1
  105. package/ts/remoteingress/classes.remoteingress-manager.ts +40 -22
  106. package/ts/security/classes.ipreputationchecker.ts +103 -196
  107. package/ts/vpn/classes.vpn-manager.ts +187 -81
  108. package/ts_web/00_commitinfo_data.ts +1 -1
  109. package/ts_web/appstate.ts +32 -0
  110. package/ts_web/elements/ops-view-vpn.ts +153 -2
  111. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  112. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  113. package/dist_ts/cache/classes.cachedb.js +0 -126
  114. package/dist_ts/cache/documents/index.d.ts +0 -2
  115. package/dist_ts/cache/documents/index.js +0 -3
  116. package/dist_ts/cache/index.js +0 -7
  117. package/dist_ts/storage/classes.storagemanager.d.ts +0 -83
  118. package/dist_ts/storage/classes.storagemanager.js +0 -348
  119. package/dist_ts/storage/index.d.ts +0 -1
  120. package/dist_ts/storage/index.js +0 -3
  121. package/ts/cache/classes.cachedb.ts +0 -155
  122. package/ts/cache/documents/index.ts +0 -2
  123. package/ts/storage/classes.storagemanager.ts +0 -404
  124. package/ts/storage/index.ts +0 -2
  125. /package/dist_ts/{cache → db}/classes.cached.document.d.ts +0 -0
  126. /package/dist_ts/{cache → db}/documents/classes.cached.email.d.ts +0 -0
  127. /package/dist_ts/{cache → db}/documents/classes.cached.ip.reputation.d.ts +0 -0
  128. /package/ts/{cache → db}/classes.cached.document.ts +0 -0
@@ -1,5 +1,5 @@
1
1
  import { logger } from './logger.js';
2
- import type { StorageManager } from './storage/index.js';
2
+ import { CertBackoffDoc } from './db/index.js';
3
3
 
4
4
  interface IBackoffEntry {
5
5
  failures: number;
@@ -10,54 +10,68 @@ interface IBackoffEntry {
10
10
 
11
11
  /**
12
12
  * Manages certificate provisioning scheduling with:
13
- * - Per-domain exponential backoff persisted in StorageManager
13
+ * - Per-domain exponential backoff persisted via CertBackoffDoc
14
14
  *
15
15
  * Note: Serial stagger queue was removed — smartacme v9 handles
16
16
  * concurrency, per-domain dedup, and rate limiting internally.
17
17
  */
18
18
  export class CertProvisionScheduler {
19
- private storageManager: StorageManager;
20
19
  private maxBackoffHours: number;
21
20
 
22
21
  // In-memory backoff cache (mirrors storage for fast lookups)
23
22
  private backoffCache = new Map<string, IBackoffEntry>();
24
23
 
25
24
  constructor(
26
- storageManager: StorageManager,
27
25
  options?: { maxBackoffHours?: number }
28
26
  ) {
29
- this.storageManager = storageManager;
30
27
  this.maxBackoffHours = options?.maxBackoffHours ?? 24;
31
28
  }
32
29
 
33
30
  /**
34
- * Storage key for a domain's backoff entry
31
+ * Sanitized domain key for storage lookups
35
32
  */
36
- private backoffKey(domain: string): string {
37
- const clean = domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
38
- return `/cert-backoff/${clean}`;
33
+ private sanitizeDomain(domain: string): string {
34
+ return domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
39
35
  }
40
36
 
41
37
  /**
42
- * Load backoff entry from storage (with in-memory cache)
38
+ * Load backoff entry from database (with in-memory cache)
43
39
  */
44
40
  private async loadBackoff(domain: string): Promise<IBackoffEntry | null> {
45
41
  const cached = this.backoffCache.get(domain);
46
42
  if (cached) return cached;
47
43
 
48
- const entry = await this.storageManager.getJSON<IBackoffEntry>(this.backoffKey(domain));
49
- if (entry) {
44
+ const sanitized = this.sanitizeDomain(domain);
45
+ const doc = await CertBackoffDoc.findByDomain(sanitized);
46
+ if (doc) {
47
+ const entry: IBackoffEntry = {
48
+ failures: doc.failures,
49
+ lastFailure: doc.lastFailure,
50
+ retryAfter: doc.retryAfter,
51
+ lastError: doc.lastError,
52
+ };
50
53
  this.backoffCache.set(domain, entry);
54
+ return entry;
51
55
  }
52
- return entry;
56
+ return null;
53
57
  }
54
58
 
55
59
  /**
56
- * Save backoff entry to both cache and storage
60
+ * Save backoff entry to both cache and database
57
61
  */
58
62
  private async saveBackoff(domain: string, entry: IBackoffEntry): Promise<void> {
59
63
  this.backoffCache.set(domain, entry);
60
- await this.storageManager.setJSON(this.backoffKey(domain), entry);
64
+ const sanitized = this.sanitizeDomain(domain);
65
+ let doc = await CertBackoffDoc.findByDomain(sanitized);
66
+ if (!doc) {
67
+ doc = new CertBackoffDoc();
68
+ doc.domain = sanitized;
69
+ }
70
+ doc.failures = entry.failures;
71
+ doc.lastFailure = entry.lastFailure;
72
+ doc.retryAfter = entry.retryAfter;
73
+ doc.lastError = entry.lastError || '';
74
+ await doc.save();
61
75
  }
62
76
 
63
77
  /**
@@ -107,9 +121,13 @@ export class CertProvisionScheduler {
107
121
  async clearBackoff(domain: string): Promise<void> {
108
122
  this.backoffCache.delete(domain);
109
123
  try {
110
- await this.storageManager.delete(this.backoffKey(domain));
124
+ const sanitized = this.sanitizeDomain(domain);
125
+ const doc = await CertBackoffDoc.findByDomain(sanitized);
126
+ if (doc) {
127
+ await doc.delete();
128
+ }
111
129
  } catch {
112
- // Ignore delete errors (key may not exist)
130
+ // Ignore delete errors (doc may not exist)
113
131
  }
114
132
  }
115
133
 
@@ -11,12 +11,10 @@ import {
11
11
  type IEmailDomainConfig,
12
12
  } from '@push.rocks/smartmta';
13
13
  import { logger } from './logger.js';
14
- // Import storage manager
15
- import { StorageManager, type IStorageConfig } from './storage/index.js';
16
14
  import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
17
15
  import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
18
- // Import cache system
19
- import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
16
+ // Import unified database
17
+ import { DcRouterDb, type IDcRouterDbConfig, CacheCleaner, ProxyCertDoc, AcmeCertDoc } from './db/index.js';
20
18
 
21
19
  import { OpsServer } from './opsserver/index.js';
22
20
  import { MetricsManager } from './monitoring/index.js';
@@ -122,37 +120,23 @@ export interface IDcRouterOptions {
122
120
  /** Other DNS providers can be added here */
123
121
  };
124
122
 
125
- /** Storage configuration */
126
- storage?: IStorageConfig;
127
-
128
123
  /**
129
- * Cache database configuration using smartdata and LocalTsmDb
130
- * Provides persistent caching for emails, IP reputation, bounces, etc.
124
+ * Unified database configuration.
125
+ * All persistent data (config, certs, VPN, cache, etc.) is stored via smartdata.
126
+ * If mongoDbUrl is provided, connects to external MongoDB.
127
+ * Otherwise, starts an embedded LocalSmartDb automatically.
131
128
  */
132
- cacheConfig?: {
133
- /** Enable cache database (default: true) */
129
+ dbConfig?: {
130
+ /** Enable database (default: true). Set to false in tests to skip DB startup. */
134
131
  enabled?: boolean;
135
- /** Storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */
132
+ /** External MongoDB connection URL. If absent, uses embedded LocalSmartDb. */
133
+ mongoDbUrl?: string;
134
+ /** Storage path for embedded database data (default: ~/.serve.zone/dcrouter/tsmdb) */
136
135
  storagePath?: string;
137
136
  /** Database name (default: dcrouter) */
138
137
  dbName?: string;
139
- /** Default TTL in days for cached items (default: 30) */
140
- defaultTTLDays?: number;
141
- /** Cleanup interval in hours (default: 1) */
138
+ /** Cache cleanup interval in hours (default: 1) */
142
139
  cleanupIntervalHours?: number;
143
- /** TTL configuration per data type (in days) */
144
- ttlConfig?: {
145
- /** Email cache TTL (default: 30 days) */
146
- emails?: number;
147
- /** IP reputation cache TTL (default: 1 day) */
148
- ipReputation?: number;
149
- /** Bounce records TTL (default: 30 days) */
150
- bounces?: number;
151
- /** DKIM keys TTL (default: 90 days) */
152
- dkimKeys?: number;
153
- /** Suppression list TTL (default: 30 days, can be permanent) */
154
- suppression?: number;
155
- };
156
140
  };
157
141
 
158
142
  /**
@@ -221,6 +205,17 @@ export interface IDcRouterOptions {
221
205
  allowList?: string[];
222
206
  blockList?: string[];
223
207
  };
208
+ /** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
209
+ * or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
210
+ forwardingMode?: 'socket' | 'bridge' | 'hybrid';
211
+ /** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
212
+ bridgeLanSubnet?: string;
213
+ /** Physical network interface for bridge mode (auto-detected if omitted) */
214
+ bridgePhysicalInterface?: string;
215
+ /** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
216
+ bridgeIpRangeStart?: number;
217
+ /** End of VPN client IP range in LAN subnet (host offset, default: 250) */
218
+ bridgeIpRangeEnd?: number;
224
219
  };
225
220
  }
226
221
 
@@ -248,12 +243,20 @@ export class DcRouter {
248
243
  public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
249
244
  public emailServer?: UnifiedEmailServer;
250
245
  public radiusServer?: RadiusServer;
251
- public storageManager: StorageManager;
252
246
  public opsServer!: OpsServer;
253
247
  public metricsManager?: MetricsManager;
254
248
 
255
- // Cache system (smartdata + LocalTsmDb)
256
- public cacheDb?: CacheDb;
249
+ // Compatibility shim for smartmta's DkimManager which calls dcRouter.storageManager.set()
250
+ public storageManager: any = {
251
+ get: async (_key: string) => null,
252
+ set: async (_key: string, _value: string) => {
253
+ // DKIM keys from smartmta — logged but not yet migrated to smartdata
254
+ logger.log('debug', `storageManager.set() called (compat shim) for key: ${_key}`);
255
+ },
256
+ };
257
+
258
+ // Unified database (smartdata + LocalSmartDb or external MongoDB)
259
+ public dcRouterDb?: DcRouterDb;
257
260
  public cacheCleaner?: CacheCleaner;
258
261
 
259
262
  // Remote Ingress
@@ -312,16 +315,6 @@ export class DcRouter {
312
315
  // Resolve all data paths from baseDir
313
316
  this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
314
317
 
315
- // Default storage to filesystem if not configured
316
- if (!this.options.storage) {
317
- this.options.storage = {
318
- fsPath: this.resolvedPaths.defaultStoragePath,
319
- };
320
- }
321
-
322
- // Initialize storage manager
323
- this.storageManager = new StorageManager(this.options.storage);
324
-
325
318
  // Initialize service manager and register all services
326
319
  this.serviceManager = new plugins.taskbuffer.ServiceManager({
327
320
  name: 'dcrouter',
@@ -350,23 +343,23 @@ export class DcRouter {
350
343
  .withRetry({ maxRetries: 0 }),
351
344
  );
352
345
 
353
- // CacheDb: optional, no dependencies
354
- if (this.options.cacheConfig?.enabled !== false) {
346
+ // DcRouterDb: optional, no dependencies — unified database for all persistence
347
+ if (this.options.dbConfig?.enabled !== false) {
355
348
  this.serviceManager.addService(
356
- new plugins.taskbuffer.Service('CacheDb')
349
+ new plugins.taskbuffer.Service('DcRouterDb')
357
350
  .optional()
358
351
  .withStart(async () => {
359
- await this.setupCacheDb();
352
+ await this.setupDcRouterDb();
360
353
  })
361
354
  .withStop(async () => {
362
355
  if (this.cacheCleaner) {
363
356
  this.cacheCleaner.stop();
364
357
  this.cacheCleaner = undefined;
365
358
  }
366
- if (this.cacheDb) {
367
- await this.cacheDb.stop();
368
- CacheDb.resetInstance();
369
- this.cacheDb = undefined;
359
+ if (this.dcRouterDb) {
360
+ await this.dcRouterDb.stop();
361
+ DcRouterDb.resetInstance();
362
+ this.dcRouterDb = undefined;
370
363
  }
371
364
  })
372
365
  .withRetry({ maxRetries: 2, baseDelayMs: 1000, maxDelayMs: 5000 }),
@@ -391,10 +384,10 @@ export class DcRouter {
391
384
  .withRetry({ maxRetries: 1, baseDelayMs: 1000 }),
392
385
  );
393
386
 
394
- // SmartProxy: critical, depends on CacheDb (if enabled)
387
+ // SmartProxy: critical, depends on DcRouterDb (if enabled)
395
388
  const smartProxyDeps: string[] = [];
396
- if (this.options.cacheConfig?.enabled !== false) {
397
- smartProxyDeps.push('CacheDb');
389
+ if (this.options.dbConfig?.enabled !== false) {
390
+ smartProxyDeps.push('DcRouterDb');
398
391
  }
399
392
  this.serviceManager.addService(
400
393
  new plugins.taskbuffer.Service('SmartProxy')
@@ -455,36 +448,38 @@ export class DcRouter {
455
448
  );
456
449
  }
457
450
 
458
- // ConfigManagers: optional, depends on SmartProxy
459
- this.serviceManager.addService(
460
- new plugins.taskbuffer.Service('ConfigManagers')
461
- .optional()
462
- .dependsOn('SmartProxy')
463
- .withStart(async () => {
464
- this.routeConfigManager = new RouteConfigManager(
465
- this.storageManager,
466
- () => this.getConstructorRoutes(),
467
- () => this.smartProxy,
468
- () => this.options.http3,
469
- this.options.vpnConfig?.enabled
470
- ? (tags?: string[]) => {
471
- if (tags?.length && this.vpnManager) {
472
- return this.vpnManager.getClientIpsForServerDefinedTags(tags);
451
+ // ConfigManagers: optional, depends on SmartProxy + DcRouterDb
452
+ // Requires DcRouterDb to be enabled (document classes need the database)
453
+ if (this.options.dbConfig?.enabled !== false) {
454
+ this.serviceManager.addService(
455
+ new plugins.taskbuffer.Service('ConfigManagers')
456
+ .optional()
457
+ .dependsOn('SmartProxy', 'DcRouterDb')
458
+ .withStart(async () => {
459
+ this.routeConfigManager = new RouteConfigManager(
460
+ () => this.getConstructorRoutes(),
461
+ () => this.smartProxy,
462
+ () => this.options.http3,
463
+ this.options.vpnConfig?.enabled
464
+ ? (tags?: string[]) => {
465
+ if (tags?.length && this.vpnManager) {
466
+ return this.vpnManager.getClientIpsForServerDefinedTags(tags);
467
+ }
468
+ return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
473
469
  }
474
- return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
475
- }
476
- : undefined,
477
- );
478
- this.apiTokenManager = new ApiTokenManager(this.storageManager);
479
- await this.apiTokenManager.initialize();
480
- await this.routeConfigManager.initialize();
481
- })
482
- .withStop(async () => {
483
- this.routeConfigManager = undefined;
484
- this.apiTokenManager = undefined;
485
- })
486
- .withRetry({ maxRetries: 2, baseDelayMs: 1000 }),
487
- );
470
+ : undefined,
471
+ );
472
+ this.apiTokenManager = new ApiTokenManager();
473
+ await this.apiTokenManager.initialize();
474
+ await this.routeConfigManager.initialize();
475
+ })
476
+ .withStop(async () => {
477
+ this.routeConfigManager = undefined;
478
+ this.apiTokenManager = undefined;
479
+ })
480
+ .withRetry({ maxRetries: 2, baseDelayMs: 1000 }),
481
+ );
482
+ }
488
483
 
489
484
  // Email Server: optional, depends on SmartProxy
490
485
  if (this.options.emailConfig) {
@@ -695,14 +690,9 @@ export class DcRouter {
695
690
  logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
696
691
  }
697
692
 
698
- // Storage summary
699
- if (this.storageManager && this.options.storage) {
700
- logger.log('info', `Storage: path=${this.options.storage.fsPath || 'default'}`);
701
- }
702
-
703
- // Cache database summary
704
- if (this.cacheDb) {
705
- logger.log('info', `Cache Database: storage=${this.cacheDb.getStoragePath()}, db=${this.cacheDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
693
+ // Database summary
694
+ if (this.dcRouterDb) {
695
+ logger.log('info', `Database: ${this.dcRouterDb.isEmbedded() ? 'embedded' : 'external'}, db=${this.dcRouterDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.dbConfig?.cleanupIntervalHours || 1)}h interval)`);
706
696
  }
707
697
 
708
698
  // Service status summary from ServiceManager
@@ -723,31 +713,32 @@ export class DcRouter {
723
713
  }
724
714
 
725
715
  /**
726
- * Set up the cache database (smartdata + LocalTsmDb)
716
+ * Set up the unified database (smartdata + LocalSmartDb or external MongoDB)
727
717
  */
728
- private async setupCacheDb(): Promise<void> {
729
- logger.log('info', 'Setting up CacheDb...');
718
+ private async setupDcRouterDb(): Promise<void> {
719
+ logger.log('info', 'Setting up DcRouterDb...');
730
720
 
731
- const cacheConfig = this.options.cacheConfig || {};
721
+ const dbConfig = this.options.dbConfig || {};
732
722
 
733
- // Initialize CacheDb singleton
734
- this.cacheDb = CacheDb.getInstance({
735
- storagePath: cacheConfig.storagePath || this.resolvedPaths.defaultTsmDbPath,
736
- dbName: cacheConfig.dbName || 'dcrouter',
723
+ // Initialize DcRouterDb singleton
724
+ this.dcRouterDb = DcRouterDb.getInstance({
725
+ mongoDbUrl: dbConfig.mongoDbUrl,
726
+ storagePath: dbConfig.storagePath || this.resolvedPaths.defaultTsmDbPath,
727
+ dbName: dbConfig.dbName || 'dcrouter',
737
728
  debug: false,
738
729
  });
739
730
 
740
- await this.cacheDb.start();
731
+ await this.dcRouterDb.start();
741
732
 
742
- // Start the cache cleaner
743
- const cleanupIntervalMs = (cacheConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
744
- this.cacheCleaner = new CacheCleaner(this.cacheDb, {
733
+ // Start the cache cleaner for TTL-based document cleanup
734
+ const cleanupIntervalMs = (dbConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
735
+ this.cacheCleaner = new CacheCleaner(this.dcRouterDb, {
745
736
  intervalMs: cleanupIntervalMs,
746
737
  verbose: false,
747
738
  });
748
739
  this.cacheCleaner.start();
749
740
 
750
- logger.log('info', `CacheDb initialized at ${this.cacheDb.getStoragePath()}`);
741
+ logger.log('info', `DcRouterDb ready (${this.dcRouterDb.isEmbedded() ? 'embedded' : 'external'})`);
751
742
  }
752
743
 
753
744
  /**
@@ -850,14 +841,11 @@ export class DcRouter {
850
841
  acme: acmeConfig,
851
842
  certStore: {
852
843
  loadAll: async () => {
853
- const keys = await this.storageManager.list('/proxy-certs/');
844
+ const docs = await ProxyCertDoc.findAll();
854
845
  const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
855
- for (const key of keys) {
856
- const data = await this.storageManager.getJSON(key);
857
- if (data) {
858
- certs.push(data);
859
- loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
860
- }
846
+ for (const doc of docs) {
847
+ certs.push({ domain: doc.domain, publicKey: doc.publicKey, privateKey: doc.privateKey, ca: doc.ca });
848
+ loadedCertEntries.push({ domain: doc.domain, publicKey: doc.publicKey, validUntil: doc.validUntil, validFrom: doc.validFrom });
861
849
  }
862
850
  return certs;
863
851
  },
@@ -869,18 +857,29 @@ export class DcRouter {
869
857
  validUntil = new Date(x509.validTo).getTime();
870
858
  validFrom = new Date(x509.validFrom).getTime();
871
859
  } catch { /* PEM parsing failed */ }
872
- await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
873
- domain, publicKey, privateKey, ca, validUntil, validFrom,
874
- });
860
+ let doc = await ProxyCertDoc.findByDomain(domain);
861
+ if (!doc) {
862
+ doc = new ProxyCertDoc();
863
+ doc.domain = domain;
864
+ }
865
+ doc.publicKey = publicKey;
866
+ doc.privateKey = privateKey;
867
+ doc.ca = ca || '';
868
+ doc.validUntil = validUntil || 0;
869
+ doc.validFrom = validFrom || 0;
870
+ await doc.save();
875
871
  },
876
872
  remove: async (domain: string) => {
877
- await this.storageManager.delete(`/proxy-certs/${domain}`);
873
+ const doc = await ProxyCertDoc.findByDomain(domain);
874
+ if (doc) {
875
+ await doc.delete();
876
+ }
878
877
  },
879
878
  },
880
879
  };
881
880
 
882
881
  // Initialize cert provision scheduler
883
- this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager);
882
+ this.certProvisionScheduler = new CertProvisionScheduler();
884
883
 
885
884
  // If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction
886
885
  // Note: SmartAcme.start() is NOT called here — it runs as a separate optional service
@@ -895,7 +894,7 @@ export class DcRouter {
895
894
  }
896
895
  this.smartAcme = new plugins.smartacme.SmartAcme({
897
896
  accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
898
- certManager: new StorageBackedCertManager(this.storageManager),
897
+ certManager: new StorageBackedCertManager(),
899
898
  environment: 'production',
900
899
  challengeHandlers: challengeHandlers,
901
900
  challengePriority: ['dns-01'],
@@ -1037,16 +1036,16 @@ export class DcRouter {
1037
1036
  issuedAt = new Date(entry.validFrom).toISOString();
1038
1037
  }
1039
1038
 
1040
- // Try SmartAcme /certs/ metadata as secondary source
1039
+ // Try SmartAcme AcmeCertDoc metadata as secondary source
1041
1040
  if (!expiryDate) {
1042
1041
  try {
1043
1042
  const cleanDomain = entry.domain.replace(/^\*\.?/, '');
1044
- const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`);
1045
- if (certMeta?.validUntil) {
1046
- expiryDate = new Date(certMeta.validUntil).toISOString();
1043
+ const certDoc = await AcmeCertDoc.findByDomain(cleanDomain);
1044
+ if (certDoc?.validUntil) {
1045
+ expiryDate = new Date(certDoc.validUntil).toISOString();
1047
1046
  }
1048
- if (certMeta?.created && !issuedAt) {
1049
- issuedAt = new Date(certMeta.created).toISOString();
1047
+ if (certDoc?.created && !issuedAt) {
1048
+ issuedAt = new Date(certDoc.created).toISOString();
1050
1049
  }
1051
1050
  } catch { /* no metadata available */ }
1052
1051
  }
@@ -2030,7 +2029,7 @@ export class DcRouter {
2030
2029
  logger.log('info', 'Setting up Remote Ingress hub...');
2031
2030
 
2032
2031
  // Initialize the edge registration manager
2033
- this.remoteIngressManager = new RemoteIngressManager(this.storageManager);
2032
+ this.remoteIngressManager = new RemoteIngressManager();
2034
2033
  await this.remoteIngressManager.initialize();
2035
2034
 
2036
2035
  // Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
@@ -2056,7 +2055,7 @@ export class DcRouter {
2056
2055
  // Priority 2: Existing cert from SmartProxy cert store for hubDomain
2057
2056
  if (!tlsConfig && riCfg.hubDomain) {
2058
2057
  try {
2059
- const stored = await this.storageManager.getJSON(`/proxy-certs/${riCfg.hubDomain}`);
2058
+ const stored = await ProxyCertDoc.findByDomain(riCfg.hubDomain);
2060
2059
  if (stored?.publicKey && stored?.privateKey) {
2061
2060
  tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
2062
2061
  logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${riCfg.hubDomain}`);
@@ -2090,13 +2089,18 @@ export class DcRouter {
2090
2089
 
2091
2090
  logger.log('info', 'Setting up VPN server...');
2092
2091
 
2093
- this.vpnManager = new VpnManager(this.storageManager, {
2092
+ this.vpnManager = new VpnManager({
2094
2093
  subnet: this.options.vpnConfig.subnet,
2095
2094
  wgListenPort: this.options.vpnConfig.wgListenPort,
2096
2095
  dns: this.options.vpnConfig.dns,
2097
2096
  serverEndpoint: this.options.vpnConfig.serverEndpoint,
2098
2097
  initialClients: this.options.vpnConfig.clients,
2099
2098
  destinationPolicy: this.options.vpnConfig.destinationPolicy,
2099
+ forwardingMode: this.options.vpnConfig.forwardingMode,
2100
+ bridgeLanSubnet: this.options.vpnConfig.bridgeLanSubnet,
2101
+ bridgePhysicalInterface: this.options.vpnConfig.bridgePhysicalInterface,
2102
+ bridgeIpRangeStart: this.options.vpnConfig.bridgeIpRangeStart,
2103
+ bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
2100
2104
  onClientChanged: () => {
2101
2105
  // Re-apply routes so tag-based ipAllowLists get updated
2102
2106
  this.routeConfigManager?.applyRoutes();
@@ -2180,7 +2184,7 @@ export class DcRouter {
2180
2184
 
2181
2185
  logger.log('info', 'Setting up RADIUS server...');
2182
2186
 
2183
- this.radiusServer = new RadiusServer(this.options.radiusConfig, this.storageManager);
2187
+ this.radiusServer = new RadiusServer(this.options.radiusConfig);
2184
2188
  await this.radiusServer.start();
2185
2189
 
2186
2190
  logger.log('info', `RADIUS server started on ports ${this.options.radiusConfig.authPort || 1812} (auth) and ${this.options.radiusConfig.acctPort || 1813} (acct)`);
@@ -1,46 +1,58 @@
1
1
  import * as plugins from './plugins.js';
2
- import { StorageManager } from './storage/index.js';
2
+ import { AcmeCertDoc } from './db/index.js';
3
3
 
4
4
  /**
5
- * ICertManager implementation backed by StorageManager.
6
- * Persists SmartAcme certificates under a /certs/ key prefix so they
5
+ * ICertManager implementation backed by smartdata document classes.
6
+ * Persists SmartAcme certificates via AcmeCertDoc so they
7
7
  * survive process restarts without re-hitting ACME.
8
8
  */
9
9
  export class StorageBackedCertManager implements plugins.smartacme.ICertManager {
10
- private keyPrefix = '/certs/';
11
-
12
- constructor(private storageManager: StorageManager) {}
10
+ constructor() {}
13
11
 
14
12
  async init(): Promise<void> {}
15
13
 
16
14
  async retrieveCertificate(domainName: string): Promise<plugins.smartacme.Cert | null> {
17
- const data = await this.storageManager.getJSON(this.keyPrefix + domainName);
18
- if (!data) return null;
19
- return new plugins.smartacme.Cert(data);
15
+ const doc = await AcmeCertDoc.findByDomain(domainName);
16
+ if (!doc) return null;
17
+ return new plugins.smartacme.Cert({
18
+ id: doc.id,
19
+ domainName: doc.domainName,
20
+ created: doc.created,
21
+ privateKey: doc.privateKey,
22
+ publicKey: doc.publicKey,
23
+ csr: doc.csr,
24
+ validUntil: doc.validUntil,
25
+ });
20
26
  }
21
27
 
22
28
  async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
23
- await this.storageManager.setJSON(this.keyPrefix + cert.domainName, {
24
- id: cert.id,
25
- domainName: cert.domainName,
26
- created: cert.created,
27
- privateKey: cert.privateKey,
28
- publicKey: cert.publicKey,
29
- csr: cert.csr,
30
- validUntil: cert.validUntil,
31
- });
29
+ let doc = await AcmeCertDoc.findByDomain(cert.domainName);
30
+ if (!doc) {
31
+ doc = new AcmeCertDoc();
32
+ doc.domainName = cert.domainName;
33
+ }
34
+ doc.id = cert.id;
35
+ doc.created = cert.created;
36
+ doc.privateKey = cert.privateKey;
37
+ doc.publicKey = cert.publicKey;
38
+ doc.csr = cert.csr;
39
+ doc.validUntil = cert.validUntil;
40
+ await doc.save();
32
41
  }
33
42
 
34
43
  async deleteCertificate(domainName: string): Promise<void> {
35
- await this.storageManager.delete(this.keyPrefix + domainName);
44
+ const doc = await AcmeCertDoc.findByDomain(domainName);
45
+ if (doc) {
46
+ await doc.delete();
47
+ }
36
48
  }
37
49
 
38
50
  async close(): Promise<void> {}
39
51
 
40
52
  async wipe(): Promise<void> {
41
- const keys = await this.storageManager.list(this.keyPrefix);
42
- for (const key of keys) {
43
- await this.storageManager.delete(key);
53
+ const docs = await AcmeCertDoc.findAll();
54
+ for (const doc of docs) {
55
+ await doc.delete();
44
56
  }
45
57
  }
46
58
  }