@serve.zone/dcrouter 13.18.0 → 13.19.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 (35) hide show
  1. package/dist_serve/bundle.js +6 -5
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +2 -0
  4. package/dist_ts/classes.dcrouter.js +50 -39
  5. package/dist_ts/config/classes.route-config-manager.d.ts +13 -5
  6. package/dist_ts/config/classes.route-config-manager.js +76 -36
  7. package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
  8. package/dist_ts/db/documents/classes.route.doc.js +11 -2
  9. package/dist_ts/email/classes.email-domain.manager.js +9 -28
  10. package/dist_ts/email/email-dns-records.d.ts +14 -0
  11. package/dist_ts/email/email-dns-records.js +34 -0
  12. package/dist_ts/email/index.d.ts +1 -0
  13. package/dist_ts/email/index.js +2 -1
  14. package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
  15. package/dist_ts_interfaces/data/route-management.d.ts +2 -0
  16. package/dist_ts_migrations/index.js +25 -1
  17. package/dist_ts_web/00_commitinfo_data.js +1 -1
  18. package/dist_ts_web/appstate.js +13 -4
  19. package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
  20. package/dist_ts_web/elements/network/ops-view-routes.js +44 -21
  21. package/package.json +2 -3
  22. package/readme.md +190 -1543
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/classes.dcrouter.ts +61 -47
  25. package/ts/config/classes.route-config-manager.ts +97 -42
  26. package/ts/db/documents/classes.route.doc.ts +7 -0
  27. package/ts/email/classes.email-domain.manager.ts +8 -28
  28. package/ts/email/email-dns-records.ts +53 -0
  29. package/ts/email/index.ts +1 -0
  30. package/ts/opsserver/handlers/route-management.handler.ts +4 -6
  31. package/ts_apiclient/readme.md +69 -195
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/appstate.ts +16 -4
  34. package/ts_web/elements/network/ops-view-routes.ts +47 -29
  35. package/ts_web/readme.md +41 -242
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.18.0',
6
+ version: '13.19.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -30,7 +30,8 @@ import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/
30
30
  import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
31
31
  import { DnsManager } from './dns/manager.dns.js';
32
32
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
33
- import { EmailDomainManager, SmartMtaStorageManager } from './email/index.js';
33
+ import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
34
+ import type { IRoute } from '../ts_interfaces/data/route-management.js';
34
35
 
35
36
  export interface IDcRouterOptions {
36
37
  /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -314,7 +315,8 @@ export class DcRouter {
314
315
  // Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
315
316
  private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
316
317
  private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
317
- // Runtime-only DoH routes. These carry live socket handlers and must never be persisted.
318
+ private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
319
+ // Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
318
320
  private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
319
321
 
320
322
  // Environment access
@@ -588,13 +590,15 @@ export class DcRouter {
588
590
  this.tunnelManager.syncAllowedEdges();
589
591
  }
590
592
  },
591
- () => this.runtimeDnsRoutes,
593
+ undefined,
594
+ (storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute),
592
595
  );
593
596
  this.apiTokenManager = new ApiTokenManager();
594
597
  await this.apiTokenManager.initialize();
595
598
  await this.routeConfigManager.initialize(
596
599
  this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
597
600
  this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
601
+ this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
598
602
  );
599
603
  await this.targetProfileManager.normalizeAllRouteRefs();
600
604
 
@@ -912,10 +916,12 @@ export class DcRouter {
912
916
  logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
913
917
  }
914
918
 
919
+ this.seedDnsRoutes = [];
915
920
  this.runtimeDnsRoutes = [];
916
921
  if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
917
- this.runtimeDnsRoutes = this.generateDnsRoutes();
918
- logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.runtimeDnsRoutes) });
922
+ this.seedDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: false });
923
+ this.runtimeDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: true });
924
+ logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) });
919
925
  }
920
926
 
921
927
  // Combined routes for SmartProxy bootstrap (before DB routes are loaded)
@@ -1338,19 +1344,20 @@ export class DcRouter {
1338
1344
  /**
1339
1345
  * Generate SmartProxy routes for DNS configuration
1340
1346
  */
1341
- private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
1347
+ private generateDnsRoutes(options?: { includeSocketHandler?: boolean }): plugins.smartproxy.IRouteConfig[] {
1342
1348
  if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
1343
1349
  return [];
1344
1350
  }
1345
-
1351
+
1352
+ const includeSocketHandler = options?.includeSocketHandler !== false;
1346
1353
  const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
1347
-
1354
+
1348
1355
  // Create routes for DNS-over-HTTPS paths
1349
1356
  const dohPaths = ['/dns-query', '/resolve'];
1350
-
1357
+
1351
1358
  // Use the first nameserver domain for DoH routes
1352
1359
  const primaryNameserver = this.options.dnsNsDomains[0];
1353
-
1360
+
1354
1361
  for (const path of dohPaths) {
1355
1362
  const dohRoute: plugins.smartproxy.IRouteConfig = {
1356
1363
  name: `dns-over-https-${path.replace('/', '')}`,
@@ -1359,18 +1366,42 @@ export class DcRouter {
1359
1366
  domains: [primaryNameserver],
1360
1367
  path: path
1361
1368
  },
1362
- action: {
1363
- type: 'socket-handler' as any,
1364
- socketHandler: this.createDnsSocketHandler()
1365
- } as any
1369
+ action: includeSocketHandler
1370
+ ? {
1371
+ type: 'socket-handler' as any,
1372
+ socketHandler: this.createDnsSocketHandler()
1373
+ } as any
1374
+ : {
1375
+ type: 'socket-handler' as any,
1376
+ } as any
1366
1377
  };
1367
-
1378
+
1368
1379
  dnsRoutes.push(dohRoute);
1369
1380
  }
1370
-
1381
+
1371
1382
  return dnsRoutes;
1372
1383
  }
1373
1384
 
1385
+ private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
1386
+ const routeName = storedRoute.route.name || '';
1387
+ const isDohRoute = storedRoute.origin === 'dns'
1388
+ && storedRoute.route.action?.type === 'socket-handler'
1389
+ && routeName.startsWith('dns-over-https-');
1390
+
1391
+ if (!isDohRoute) {
1392
+ return undefined;
1393
+ }
1394
+
1395
+ return {
1396
+ ...storedRoute.route,
1397
+ action: {
1398
+ ...storedRoute.route.action,
1399
+ type: 'socket-handler' as any,
1400
+ socketHandler: this.createDnsSocketHandler(),
1401
+ } as any,
1402
+ };
1403
+ }
1404
+
1374
1405
  /**
1375
1406
  * Check if a domain matches a pattern (including wildcard support)
1376
1407
  * @param domain The domain to check
@@ -1939,37 +1970,20 @@ export class DcRouter {
1939
1970
  for (const domainConfig of internalDnsDomains) {
1940
1971
  const domain = domainConfig.domain;
1941
1972
  const ttl = domainConfig.dns?.internal?.ttl || 3600;
1942
- const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
1943
-
1944
- // MX record - points to the domain itself for email handling
1945
- records.push({
1946
- name: domain,
1947
- type: 'MX',
1948
- value: `${mxPriority} ${domain}`,
1949
- ttl
1950
- });
1951
-
1952
- // SPF record - using sensible defaults
1953
- const spfRecord = 'v=spf1 a mx ~all';
1954
- records.push({
1955
- name: domain,
1956
- type: 'TXT',
1957
- value: spfRecord,
1958
- ttl
1959
- });
1960
-
1961
- // DMARC record - using sensible defaults
1962
- const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring
1963
- const dmarcEmail = `dmarc@${domain}`;
1964
- records.push({
1965
- name: `_dmarc.${domain}`,
1966
- type: 'TXT',
1967
- value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`,
1968
- ttl
1969
- });
1970
-
1971
- // Note: DKIM records will be generated later when DKIM keys are available
1972
- // They require the DKIMCreator which is part of the email server
1973
+ const requiredRecords = buildEmailDnsRecords({
1974
+ domain,
1975
+ hostname: this.options.emailConfig.hostname,
1976
+ mxPriority: domainConfig.dns?.internal?.mxPriority,
1977
+ }).filter((record) => !record.name.includes('._domainkey.'));
1978
+
1979
+ for (const record of requiredRecords) {
1980
+ records.push({
1981
+ name: record.name,
1982
+ type: record.type,
1983
+ value: record.value,
1984
+ ttl,
1985
+ });
1986
+ }
1973
1987
  }
1974
1988
 
1975
1989
  logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
@@ -14,6 +14,11 @@ import type { ReferenceResolver } from './classes.reference-resolver.js';
14
14
  /** An IP allow entry: plain IP/CIDR or domain-scoped. */
15
15
  export type TIpAllowEntry = string | { ip: string; domains: string[] };
16
16
 
17
+ export interface IRouteMutationResult {
18
+ success: boolean;
19
+ message?: string;
20
+ }
21
+
17
22
  /**
18
23
  * Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
19
24
  * never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
@@ -56,6 +61,7 @@ export class RouteConfigManager {
56
61
  private referenceResolver?: ReferenceResolver,
57
62
  private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
58
63
  private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
64
+ private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
59
65
  ) {}
60
66
 
61
67
  /** Expose routes map for reference resolution lookups. */
@@ -63,6 +69,10 @@ export class RouteConfigManager {
63
69
  return this.routes;
64
70
  }
65
71
 
72
+ public getRoute(id: string): IRoute | undefined {
73
+ return this.routes.get(id);
74
+ }
75
+
66
76
  /**
67
77
  * Load persisted routes, seed serializable config/email/dns routes,
68
78
  * compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
@@ -94,6 +104,7 @@ export class RouteConfigManager {
94
104
  id: route.id,
95
105
  enabled: route.enabled,
96
106
  origin: route.origin,
107
+ systemKey: route.systemKey,
97
108
  createdAt: route.createdAt,
98
109
  updatedAt: route.updatedAt,
99
110
  metadata: route.metadata,
@@ -153,9 +164,21 @@ export class RouteConfigManager {
153
164
  enabled?: boolean;
154
165
  metadata?: Partial<IRouteMetadata>;
155
166
  },
156
- ): Promise<boolean> {
167
+ ): Promise<IRouteMutationResult> {
157
168
  const stored = this.routes.get(id);
158
- if (!stored) return false;
169
+ if (!stored) {
170
+ return { success: false, message: 'Route not found' };
171
+ }
172
+
173
+ const isToggleOnlyPatch = patch.enabled !== undefined
174
+ && patch.route === undefined
175
+ && patch.metadata === undefined;
176
+ if (stored.origin !== 'api' && !isToggleOnlyPatch) {
177
+ return {
178
+ success: false,
179
+ message: 'System routes are managed by the system and can only be toggled',
180
+ };
181
+ }
159
182
 
160
183
  if (patch.route) {
161
184
  const mergedAction = patch.route.action
@@ -189,19 +212,29 @@ export class RouteConfigManager {
189
212
 
190
213
  await this.persistRoute(stored);
191
214
  await this.applyRoutes();
192
- return true;
215
+ return { success: true };
193
216
  }
194
217
 
195
- public async deleteRoute(id: string): Promise<boolean> {
196
- if (!this.routes.has(id)) return false;
218
+ public async deleteRoute(id: string): Promise<IRouteMutationResult> {
219
+ const stored = this.routes.get(id);
220
+ if (!stored) {
221
+ return { success: false, message: 'Route not found' };
222
+ }
223
+ if (stored.origin !== 'api') {
224
+ return {
225
+ success: false,
226
+ message: 'System routes are managed by the system and cannot be deleted',
227
+ };
228
+ }
229
+
197
230
  this.routes.delete(id);
198
231
  const doc = await RouteDoc.findById(id);
199
232
  if (doc) await doc.delete();
200
233
  await this.applyRoutes();
201
- return true;
234
+ return { success: true };
202
235
  }
203
236
 
204
- public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
237
+ public async toggleRoute(id: string, enabled: boolean): Promise<IRouteMutationResult> {
205
238
  return this.updateRoute(id, { enabled });
206
239
  }
207
240
 
@@ -217,29 +250,28 @@ export class RouteConfigManager {
217
250
  seedRoutes: IDcRouterRouteConfig[],
218
251
  origin: 'config' | 'email' | 'dns',
219
252
  ): Promise<void> {
220
- if (seedRoutes.length === 0) return;
221
-
253
+ const seedSystemKeys = new Set<string>();
222
254
  const seedNames = new Set<string>();
223
255
  let seeded = 0;
224
256
  let updated = 0;
225
257
 
226
258
  for (const route of seedRoutes) {
227
259
  const name = route.name || '';
228
- seedNames.add(name);
229
-
230
- // Check if a route with this name+origin already exists in memory
231
- let existingId: string | undefined;
232
- for (const [id, r] of this.routes) {
233
- if (r.origin === origin && r.route.name === name) {
234
- existingId = id;
235
- break;
236
- }
260
+ if (name) {
261
+ seedNames.add(name);
262
+ }
263
+ const systemKey = this.buildSystemRouteKey(origin, route);
264
+ if (systemKey) {
265
+ seedSystemKeys.add(systemKey);
237
266
  }
238
267
 
268
+ const existingId = this.findExistingSeedRouteId(origin, route, systemKey);
269
+
239
270
  if (existingId) {
240
271
  // Update route config but preserve enabled state
241
272
  const existing = this.routes.get(existingId)!;
242
273
  existing.route = route;
274
+ existing.systemKey = systemKey;
243
275
  existing.updatedAt = Date.now();
244
276
  await this.persistRoute(existing);
245
277
  updated++;
@@ -255,6 +287,7 @@ export class RouteConfigManager {
255
287
  updatedAt: now,
256
288
  createdBy: 'system',
257
289
  origin,
290
+ systemKey,
258
291
  };
259
292
  this.routes.set(id, newRoute);
260
293
  await this.persistRoute(newRoute);
@@ -265,7 +298,12 @@ export class RouteConfigManager {
265
298
  // Delete stale routes: same origin but name not in current seed set
266
299
  const staleIds: string[] = [];
267
300
  for (const [id, r] of this.routes) {
268
- if (r.origin === origin && !seedNames.has(r.route.name || '')) {
301
+ if (r.origin !== origin) continue;
302
+
303
+ const routeName = r.route.name || '';
304
+ const matchesSeedSystemKey = r.systemKey ? seedSystemKeys.has(r.systemKey) : false;
305
+ const matchesSeedName = routeName ? seedNames.has(routeName) : false;
306
+ if (!matchesSeedSystemKey && !matchesSeedName) {
269
307
  staleIds.push(id);
270
308
  }
271
309
  }
@@ -284,9 +322,39 @@ export class RouteConfigManager {
284
322
  // Private: persistence
285
323
  // =========================================================================
286
324
 
325
+ private buildSystemRouteKey(
326
+ origin: 'config' | 'email' | 'dns',
327
+ route: IDcRouterRouteConfig,
328
+ ): string | undefined {
329
+ const name = route.name?.trim();
330
+ if (!name) return undefined;
331
+ return `${origin}:${name}`;
332
+ }
333
+
334
+ private findExistingSeedRouteId(
335
+ origin: 'config' | 'email' | 'dns',
336
+ route: IDcRouterRouteConfig,
337
+ systemKey?: string,
338
+ ): string | undefined {
339
+ const routeName = route.name || '';
340
+
341
+ for (const [id, storedRoute] of this.routes) {
342
+ if (storedRoute.origin !== origin) continue;
343
+
344
+ if (systemKey && storedRoute.systemKey === systemKey) {
345
+ return id;
346
+ }
347
+
348
+ if (storedRoute.route.name === routeName) {
349
+ return id;
350
+ }
351
+ }
352
+
353
+ return undefined;
354
+ }
355
+
287
356
  private async loadRoutes(): Promise<void> {
288
357
  const docs = await RouteDoc.findAll();
289
- let prunedRuntimeRoutes = 0;
290
358
 
291
359
  for (const doc of docs) {
292
360
  if (!doc.id) continue;
@@ -299,27 +367,15 @@ export class RouteConfigManager {
299
367
  updatedAt: doc.updatedAt,
300
368
  createdBy: doc.createdBy,
301
369
  origin: doc.origin || 'api',
370
+ systemKey: doc.systemKey,
302
371
  metadata: doc.metadata,
303
372
  };
304
373
 
305
- if (this.isPersistedRuntimeRoute(storedRoute)) {
306
- await doc.delete();
307
- prunedRuntimeRoutes++;
308
- logger.log(
309
- 'warn',
310
- `Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`,
311
- );
312
- continue;
313
- }
314
-
315
374
  this.routes.set(doc.id, storedRoute);
316
375
  }
317
376
  if (this.routes.size > 0) {
318
377
  logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
319
378
  }
320
- if (prunedRuntimeRoutes > 0) {
321
- logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`);
322
- }
323
379
  }
324
380
 
325
381
  private async persistRoute(stored: IRoute): Promise<void> {
@@ -330,6 +386,7 @@ export class RouteConfigManager {
330
386
  existingDoc.updatedAt = stored.updatedAt;
331
387
  existingDoc.createdBy = stored.createdBy;
332
388
  existingDoc.origin = stored.origin;
389
+ existingDoc.systemKey = stored.systemKey;
333
390
  existingDoc.metadata = stored.metadata;
334
391
  await existingDoc.save();
335
392
  } else {
@@ -341,6 +398,7 @@ export class RouteConfigManager {
341
398
  doc.updatedAt = stored.updatedAt;
342
399
  doc.createdBy = stored.createdBy;
343
400
  doc.origin = stored.origin;
401
+ doc.systemKey = stored.systemKey;
344
402
  doc.metadata = stored.metadata;
345
403
  await doc.save();
346
404
  }
@@ -411,7 +469,7 @@ export class RouteConfigManager {
411
469
  // Add all enabled routes with HTTP/3 and VPN augmentation
412
470
  for (const route of this.routes.values()) {
413
471
  if (route.enabled) {
414
- enabledRoutes.push(this.prepareRouteForApply(route.route, route.id));
472
+ enabledRoutes.push(this.prepareStoredRouteForApply(route));
415
473
  }
416
474
  }
417
475
 
@@ -431,6 +489,11 @@ export class RouteConfigManager {
431
489
  });
432
490
  }
433
491
 
492
+ private prepareStoredRouteForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig {
493
+ const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
494
+ return this.prepareRouteForApply(hydratedRoute || storedRoute.route, storedRoute.id);
495
+ }
496
+
434
497
  private prepareRouteForApply(
435
498
  route: plugins.smartproxy.IRouteConfig,
436
499
  routeId?: string,
@@ -465,12 +528,4 @@ export class RouteConfigManager {
465
528
  },
466
529
  };
467
530
  }
468
-
469
- private isPersistedRuntimeRoute(storedRoute: IRoute): boolean {
470
- const routeName = storedRoute.route.name || '';
471
- const actionType = storedRoute.route.action?.type;
472
-
473
- return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler')
474
- || (storedRoute.origin === 'dns' && actionType === 'socket-handler');
475
- }
476
531
  }
@@ -29,6 +29,9 @@ export class RouteDoc extends plugins.smartdata.SmartDataDbDoc<RouteDoc, RouteDo
29
29
  @plugins.smartdata.svDb()
30
30
  public origin!: 'config' | 'email' | 'dns' | 'api';
31
31
 
32
+ @plugins.smartdata.svDb()
33
+ public systemKey?: string;
34
+
32
35
  @plugins.smartdata.svDb()
33
36
  public metadata?: IRouteMetadata;
34
37
 
@@ -51,4 +54,8 @@ export class RouteDoc extends plugins.smartdata.SmartDataDbDoc<RouteDoc, RouteDo
51
54
  public static async findByOrigin(origin: 'config' | 'email' | 'dns' | 'api'): Promise<RouteDoc[]> {
52
55
  return await RouteDoc.getInstances({ origin });
53
56
  }
57
+
58
+ public static async findBySystemKey(systemKey: string): Promise<RouteDoc | null> {
59
+ return await RouteDoc.getInstance({ systemKey });
60
+ }
54
61
  }
@@ -6,6 +6,7 @@ import { DomainDoc } from '../db/documents/classes.domain.doc.js';
6
6
  import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
7
7
  import type { DnsManager } from '../dns/manager.dns.js';
8
8
  import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js';
9
+ import { buildEmailDnsRecords } from './email-dns-records.js';
9
10
 
10
11
  /**
11
12
  * EmailDomainManager — orchestrates email domain setup.
@@ -181,34 +182,13 @@ export class EmailDomainManager {
181
182
  }
182
183
  }
183
184
 
184
- const records: IEmailDnsRecord[] = [
185
- {
186
- type: 'MX',
187
- name: domain,
188
- value: `10 ${hostname}`,
189
- status: doc.dnsStatus.mx,
190
- },
191
- {
192
- type: 'TXT',
193
- name: domain,
194
- value: 'v=spf1 a mx ~all',
195
- status: doc.dnsStatus.spf,
196
- },
197
- {
198
- type: 'TXT',
199
- name: `${selector}._domainkey.${domain}`,
200
- value: dkimValue,
201
- status: doc.dnsStatus.dkim,
202
- },
203
- {
204
- type: 'TXT',
205
- name: `_dmarc.${domain}`,
206
- value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`,
207
- status: doc.dnsStatus.dmarc,
208
- },
209
- ];
210
-
211
- return records;
185
+ return buildEmailDnsRecords({
186
+ domain,
187
+ hostname,
188
+ selector,
189
+ dkimValue,
190
+ statuses: doc.dnsStatus,
191
+ });
212
192
  }
213
193
 
214
194
  // ---------------------------------------------------------------------------
@@ -0,0 +1,53 @@
1
+ import type {
2
+ IEmailDnsRecord,
3
+ TDnsRecordStatus,
4
+ } from '../../ts_interfaces/data/email-domain.js';
5
+
6
+ type TEmailDnsStatusKey = 'mx' | 'spf' | 'dkim' | 'dmarc';
7
+
8
+ export interface IBuildEmailDnsRecordsOptions {
9
+ domain: string;
10
+ hostname: string;
11
+ selector?: string;
12
+ dkimValue?: string;
13
+ mxPriority?: number;
14
+ dmarcPolicy?: string;
15
+ dmarcRua?: string;
16
+ statuses?: Partial<Record<TEmailDnsStatusKey, TDnsRecordStatus>>;
17
+ }
18
+
19
+ export function buildEmailDnsRecords(options: IBuildEmailDnsRecordsOptions): IEmailDnsRecord[] {
20
+ const statusFor = (key: TEmailDnsStatusKey): TDnsRecordStatus => options.statuses?.[key] ?? 'unchecked';
21
+ const selector = options.selector || 'default';
22
+ const records: IEmailDnsRecord[] = [
23
+ {
24
+ type: 'MX',
25
+ name: options.domain,
26
+ value: `${options.mxPriority ?? 10} ${options.hostname}`,
27
+ status: statusFor('mx'),
28
+ },
29
+ {
30
+ type: 'TXT',
31
+ name: options.domain,
32
+ value: 'v=spf1 a mx ~all',
33
+ status: statusFor('spf'),
34
+ },
35
+ {
36
+ type: 'TXT',
37
+ name: `_dmarc.${options.domain}`,
38
+ value: `v=DMARC1; p=${options.dmarcPolicy ?? 'none'}; rua=mailto:${options.dmarcRua ?? `dmarc@${options.domain}`}`,
39
+ status: statusFor('dmarc'),
40
+ },
41
+ ];
42
+
43
+ if (options.dkimValue) {
44
+ records.splice(2, 0, {
45
+ type: 'TXT',
46
+ name: `${selector}._domainkey.${options.domain}`,
47
+ value: options.dkimValue,
48
+ status: statusFor('dkim'),
49
+ });
50
+ }
51
+
52
+ return records;
53
+ }
package/ts/email/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './classes.email-domain.manager.js';
2
2
  export * from './classes.smartmta-storage-manager.js';
3
+ export * from './email-dns-records.js';
@@ -87,12 +87,12 @@ export class RouteManagementHandler {
87
87
  if (!manager) {
88
88
  return { success: false, message: 'Route management not initialized' };
89
89
  }
90
- const ok = await manager.updateRoute(dataArg.id, {
90
+ const result = await manager.updateRoute(dataArg.id, {
91
91
  route: dataArg.route as any,
92
92
  enabled: dataArg.enabled,
93
93
  metadata: dataArg.metadata,
94
94
  });
95
- return { success: ok, message: ok ? undefined : 'Route not found' };
95
+ return result;
96
96
  },
97
97
  ),
98
98
  );
@@ -107,8 +107,7 @@ export class RouteManagementHandler {
107
107
  if (!manager) {
108
108
  return { success: false, message: 'Route management not initialized' };
109
109
  }
110
- const ok = await manager.deleteRoute(dataArg.id);
111
- return { success: ok, message: ok ? undefined : 'Route not found' };
110
+ return manager.deleteRoute(dataArg.id);
112
111
  },
113
112
  ),
114
113
  );
@@ -123,8 +122,7 @@ export class RouteManagementHandler {
123
122
  if (!manager) {
124
123
  return { success: false, message: 'Route management not initialized' };
125
124
  }
126
- const ok = await manager.toggleRoute(dataArg.id, dataArg.enabled);
127
- return { success: ok, message: ok ? undefined : 'Route not found' };
125
+ return manager.toggleRoute(dataArg.id, dataArg.enabled);
128
126
  },
129
127
  ),
130
128
  );