@serve.zone/dcrouter 13.10.0 → 13.12.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 (51) hide show
  1. package/dist_serve/bundle.js +1075 -967
  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 +15 -1
  5. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +17 -0
  6. package/dist_ts/db/documents/classes.email-domain.doc.js +124 -0
  7. package/dist_ts/db/documents/index.d.ts +1 -0
  8. package/dist_ts/db/documents/index.js +3 -1
  9. package/dist_ts/email/classes.email-domain.manager.d.ts +46 -0
  10. package/dist_ts/email/classes.email-domain.manager.js +276 -0
  11. package/dist_ts/email/index.d.ts +1 -0
  12. package/dist_ts/email/index.js +2 -0
  13. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  14. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  15. package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +16 -0
  16. package/dist_ts/opsserver/handlers/email-domain.handler.js +150 -0
  17. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  18. package/dist_ts/opsserver/handlers/index.js +2 -1
  19. package/dist_ts_interfaces/data/email-domain.d.ts +70 -0
  20. package/dist_ts_interfaces/data/email-domain.js +2 -0
  21. package/dist_ts_interfaces/data/index.d.ts +1 -0
  22. package/dist_ts_interfaces/data/index.js +2 -1
  23. package/dist_ts_interfaces/requests/email-domains.d.ts +142 -0
  24. package/dist_ts_interfaces/requests/email-domains.js +2 -0
  25. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  26. package/dist_ts_interfaces/requests/index.js +2 -1
  27. package/dist_ts_web/00_commitinfo_data.js +1 -1
  28. package/dist_ts_web/appstate.d.ts +21 -0
  29. package/dist_ts_web/appstate.js +81 -1
  30. package/dist_ts_web/elements/email/index.d.ts +1 -0
  31. package/dist_ts_web/elements/email/index.js +2 -1
  32. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +19 -0
  33. package/dist_ts_web/elements/email/ops-view-email-domains.js +410 -0
  34. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  35. package/dist_ts_web/router.js +2 -2
  36. package/package.json +2 -2
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.dcrouter.ts +17 -0
  39. package/ts/db/documents/classes.email-domain.doc.ts +56 -0
  40. package/ts/db/documents/index.ts +3 -0
  41. package/ts/email/classes.email-domain.manager.ts +321 -0
  42. package/ts/email/index.ts +1 -0
  43. package/ts/opsserver/classes.opsserver.ts +2 -0
  44. package/ts/opsserver/handlers/email-domain.handler.ts +195 -0
  45. package/ts/opsserver/handlers/index.ts +2 -1
  46. package/ts_web/00_commitinfo_data.ts +1 -1
  47. package/ts_web/appstate.ts +124 -0
  48. package/ts_web/elements/email/index.ts +1 -0
  49. package/ts_web/elements/email/ops-view-email-domains.ts +396 -0
  50. package/ts_web/elements/ops-dashboard.ts +2 -0
  51. package/ts_web/router.ts +1 -1
@@ -0,0 +1,56 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type {
4
+ IEmailDomainDkim,
5
+ IEmailDomainRateLimits,
6
+ IEmailDomainDnsStatus,
7
+ } from '../../../ts_interfaces/data/email-domain.js';
8
+
9
+ const getDb = () => DcRouterDb.getInstance().getDb();
10
+
11
+ @plugins.smartdata.Collection(() => getDb())
12
+ export class EmailDomainDoc extends plugins.smartdata.SmartDataDbDoc<EmailDomainDoc, EmailDomainDoc> {
13
+ @plugins.smartdata.unI()
14
+ @plugins.smartdata.svDb()
15
+ public id!: string;
16
+
17
+ @plugins.smartdata.svDb()
18
+ public domain: string = '';
19
+
20
+ @plugins.smartdata.svDb()
21
+ public linkedDomainId: string = '';
22
+
23
+ @plugins.smartdata.svDb()
24
+ public subdomain?: string;
25
+
26
+ @plugins.smartdata.svDb()
27
+ public dkim!: IEmailDomainDkim;
28
+
29
+ @plugins.smartdata.svDb()
30
+ public rateLimits?: IEmailDomainRateLimits;
31
+
32
+ @plugins.smartdata.svDb()
33
+ public dnsStatus!: IEmailDomainDnsStatus;
34
+
35
+ @plugins.smartdata.svDb()
36
+ public createdAt!: string;
37
+
38
+ @plugins.smartdata.svDb()
39
+ public updatedAt!: string;
40
+
41
+ constructor() {
42
+ super();
43
+ }
44
+
45
+ public static async findById(id: string): Promise<EmailDomainDoc | null> {
46
+ return await EmailDomainDoc.getInstance({ id });
47
+ }
48
+
49
+ public static async findByDomain(domain: string): Promise<EmailDomainDoc | null> {
50
+ return await EmailDomainDoc.getInstance({ domain: domain.toLowerCase() });
51
+ }
52
+
53
+ public static async findAll(): Promise<EmailDomainDoc[]> {
54
+ return await EmailDomainDoc.getInstances({});
55
+ }
56
+ }
@@ -33,3 +33,6 @@ export * from './classes.dns-record.doc.js';
33
33
 
34
34
  // ACME configuration (singleton)
35
35
  export * from './classes.acme-config.doc.js';
36
+
37
+ // Email domain management
38
+ export * from './classes.email-domain.doc.js';
@@ -0,0 +1,321 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js';
4
+ import { DomainDoc } from '../db/documents/classes.domain.doc.js';
5
+ import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
6
+ import type { DnsManager } from '../dns/manager.dns.js';
7
+ import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js';
8
+
9
+ /**
10
+ * EmailDomainManager — orchestrates email domain setup.
11
+ *
12
+ * Wires smartmta's DKIMCreator (key generation) with dcrouter's DnsManager
13
+ * (record creation for dcrouter-hosted and provider-managed zones) to provide
14
+ * a single entry point for setting up an email domain from A to Z.
15
+ */
16
+ export class EmailDomainManager {
17
+ private dcRouter: any; // DcRouter — avoids circular import
18
+
19
+ constructor(dcRouterRef: any) {
20
+ this.dcRouter = dcRouterRef;
21
+ }
22
+
23
+ private get dnsManager(): DnsManager | undefined {
24
+ return this.dcRouter.dnsManager;
25
+ }
26
+
27
+ private get dkimCreator(): any | undefined {
28
+ return this.dcRouter.emailServer?.dkimCreator;
29
+ }
30
+
31
+ private get emailHostname(): string {
32
+ return this.dcRouter.options?.emailConfig?.hostname || this.dcRouter.options?.tls?.domain || 'localhost';
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // CRUD
37
+ // ---------------------------------------------------------------------------
38
+
39
+ public async getAll(): Promise<IEmailDomain[]> {
40
+ const docs = await EmailDomainDoc.findAll();
41
+ return docs.map((d) => this.docToInterface(d));
42
+ }
43
+
44
+ public async getById(id: string): Promise<IEmailDomain | null> {
45
+ const doc = await EmailDomainDoc.findById(id);
46
+ return doc ? this.docToInterface(doc) : null;
47
+ }
48
+
49
+ public async createEmailDomain(opts: {
50
+ linkedDomainId: string;
51
+ subdomain?: string;
52
+ dkimSelector?: string;
53
+ dkimKeySize?: number;
54
+ rotateKeys?: boolean;
55
+ rotationIntervalDays?: number;
56
+ }): Promise<IEmailDomain> {
57
+ // Resolve the linked DNS domain
58
+ const domainDoc = await DomainDoc.findById(opts.linkedDomainId);
59
+ if (!domainDoc) {
60
+ throw new Error(`DNS domain not found: ${opts.linkedDomainId}`);
61
+ }
62
+ const baseDomain = domainDoc.name;
63
+ const subdomain = opts.subdomain?.trim() || undefined;
64
+ const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;
65
+
66
+ // Check for duplicates
67
+ const existing = await EmailDomainDoc.findByDomain(domainName);
68
+ if (existing) {
69
+ throw new Error(`Email domain already exists for ${domainName}`);
70
+ }
71
+
72
+ const selector = opts.dkimSelector || 'default';
73
+ const keySize = opts.dkimKeySize || 2048;
74
+ const now = new Date().toISOString();
75
+
76
+ // Generate DKIM keys
77
+ let publicKey: string | undefined;
78
+ if (this.dkimCreator) {
79
+ try {
80
+ await this.dkimCreator.handleDKIMKeysForDomain(domainName);
81
+ const dnsRecord = await this.dkimCreator.getDNSRecordForSelector(domainName, selector);
82
+ // Extract public key from the DNS record value
83
+ const match = dnsRecord?.value?.match(/p=([A-Za-z0-9+/=]+)/);
84
+ publicKey = match ? match[1] : undefined;
85
+ logger.log('info', `DKIM keys generated for ${domainName} (selector: ${selector})`);
86
+ } catch (err: unknown) {
87
+ logger.log('warn', `DKIM key generation failed for ${domainName}: ${(err as Error).message}`);
88
+ }
89
+ }
90
+
91
+ // Create the document
92
+ const doc = new EmailDomainDoc();
93
+ doc.id = plugins.smartunique.shortId();
94
+ doc.domain = domainName.toLowerCase();
95
+ doc.linkedDomainId = opts.linkedDomainId;
96
+ doc.subdomain = subdomain;
97
+ doc.dkim = {
98
+ selector,
99
+ keySize,
100
+ publicKey,
101
+ rotateKeys: opts.rotateKeys ?? false,
102
+ rotationIntervalDays: opts.rotationIntervalDays ?? 90,
103
+ };
104
+ doc.dnsStatus = {
105
+ mx: 'unchecked',
106
+ spf: 'unchecked',
107
+ dkim: 'unchecked',
108
+ dmarc: 'unchecked',
109
+ };
110
+ doc.createdAt = now;
111
+ doc.updatedAt = now;
112
+ await doc.save();
113
+
114
+ logger.log('info', `Email domain created: ${domainName}`);
115
+ return this.docToInterface(doc);
116
+ }
117
+
118
+ public async updateEmailDomain(
119
+ id: string,
120
+ changes: {
121
+ rotateKeys?: boolean;
122
+ rotationIntervalDays?: number;
123
+ rateLimits?: IEmailDomain['rateLimits'];
124
+ },
125
+ ): Promise<void> {
126
+ const doc = await EmailDomainDoc.findById(id);
127
+ if (!doc) throw new Error(`Email domain not found: ${id}`);
128
+
129
+ if (changes.rotateKeys !== undefined) doc.dkim.rotateKeys = changes.rotateKeys;
130
+ if (changes.rotationIntervalDays !== undefined) doc.dkim.rotationIntervalDays = changes.rotationIntervalDays;
131
+ if (changes.rateLimits !== undefined) doc.rateLimits = changes.rateLimits;
132
+ doc.updatedAt = new Date().toISOString();
133
+ await doc.save();
134
+ }
135
+
136
+ public async deleteEmailDomain(id: string): Promise<void> {
137
+ const doc = await EmailDomainDoc.findById(id);
138
+ if (!doc) throw new Error(`Email domain not found: ${id}`);
139
+ await doc.delete();
140
+ logger.log('info', `Email domain deleted: ${doc.domain}`);
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // DNS record computation
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Compute the 4 required DNS records for an email domain.
149
+ */
150
+ public async getRequiredDnsRecords(id: string): Promise<IEmailDnsRecord[]> {
151
+ const doc = await EmailDomainDoc.findById(id);
152
+ if (!doc) throw new Error(`Email domain not found: ${id}`);
153
+
154
+ const domain = doc.domain;
155
+ const selector = doc.dkim.selector;
156
+ const publicKey = doc.dkim.publicKey || '';
157
+ const hostname = this.emailHostname;
158
+
159
+ const records: IEmailDnsRecord[] = [
160
+ {
161
+ type: 'MX',
162
+ name: domain,
163
+ value: `10 ${hostname}`,
164
+ status: doc.dnsStatus.mx,
165
+ },
166
+ {
167
+ type: 'TXT',
168
+ name: domain,
169
+ value: 'v=spf1 a mx ~all',
170
+ status: doc.dnsStatus.spf,
171
+ },
172
+ {
173
+ type: 'TXT',
174
+ name: `${selector}._domainkey.${domain}`,
175
+ value: `v=DKIM1; h=sha256; k=rsa; p=${publicKey}`,
176
+ status: doc.dnsStatus.dkim,
177
+ },
178
+ {
179
+ type: 'TXT',
180
+ name: `_dmarc.${domain}`,
181
+ value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`,
182
+ status: doc.dnsStatus.dmarc,
183
+ },
184
+ ];
185
+
186
+ return records;
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // DNS provisioning
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Auto-create missing DNS records via the linked domain's DNS path.
195
+ */
196
+ public async provisionDnsRecords(id: string): Promise<number> {
197
+ const doc = await EmailDomainDoc.findById(id);
198
+ if (!doc) throw new Error(`Email domain not found: ${id}`);
199
+ if (!this.dnsManager) throw new Error('DnsManager not available');
200
+
201
+ const requiredRecords = await this.getRequiredDnsRecords(id);
202
+ const domainId = doc.linkedDomainId;
203
+
204
+ // Get existing DNS records for the linked domain
205
+ const existingRecords = await DnsRecordDoc.findByDomainId(domainId);
206
+ let provisioned = 0;
207
+
208
+ for (const required of requiredRecords) {
209
+ // Check if a matching record already exists
210
+ const exists = existingRecords.some((r) => {
211
+ if (required.type === 'MX') {
212
+ return r.type === 'MX' && r.name.toLowerCase() === required.name.toLowerCase();
213
+ }
214
+ // For TXT records, match by name AND check value prefix (v=spf1, v=DKIM1, v=DMARC1)
215
+ if (r.type !== 'TXT' || r.name.toLowerCase() !== required.name.toLowerCase()) return false;
216
+ if (required.value.startsWith('v=spf1')) return r.value.includes('v=spf1');
217
+ if (required.value.startsWith('v=DKIM1')) return r.value.includes('v=DKIM1');
218
+ if (required.value.startsWith('v=DMARC1')) return r.value.includes('v=DMARC1');
219
+ return false;
220
+ });
221
+
222
+ if (!exists) {
223
+ try {
224
+ await this.dnsManager.createRecord({
225
+ domainId,
226
+ name: required.name,
227
+ type: required.type as any,
228
+ value: required.value,
229
+ ttl: 3600,
230
+ createdBy: 'email-domain-manager',
231
+ });
232
+ provisioned++;
233
+ logger.log('info', `Provisioned ${required.type} record for ${required.name}`);
234
+ } catch (err: unknown) {
235
+ logger.log('warn', `Failed to provision ${required.type} for ${required.name}: ${(err as Error).message}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ // Re-validate after provisioning
241
+ await this.validateDns(id);
242
+
243
+ return provisioned;
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // DNS validation
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /**
251
+ * Validate DNS records via live lookups.
252
+ */
253
+ public async validateDns(id: string): Promise<IEmailDnsRecord[]> {
254
+ const doc = await EmailDomainDoc.findById(id);
255
+ if (!doc) throw new Error(`Email domain not found: ${id}`);
256
+
257
+ const domain = doc.domain;
258
+ const selector = doc.dkim.selector;
259
+ const resolver = new plugins.dns.promises.Resolver();
260
+
261
+ // MX check
262
+ doc.dnsStatus.mx = await this.checkMx(resolver, domain);
263
+
264
+ // SPF check
265
+ doc.dnsStatus.spf = await this.checkTxtRecord(resolver, domain, 'v=spf1');
266
+
267
+ // DKIM check
268
+ doc.dnsStatus.dkim = await this.checkTxtRecord(resolver, `${selector}._domainkey.${domain}`, 'v=DKIM1');
269
+
270
+ // DMARC check
271
+ doc.dnsStatus.dmarc = await this.checkTxtRecord(resolver, `_dmarc.${domain}`, 'v=DMARC1');
272
+
273
+ doc.dnsStatus.lastCheckedAt = new Date().toISOString();
274
+ doc.updatedAt = new Date().toISOString();
275
+ await doc.save();
276
+
277
+ return this.getRequiredDnsRecords(id);
278
+ }
279
+
280
+ private async checkMx(resolver: plugins.dns.promises.Resolver, domain: string): Promise<TDnsRecordStatus> {
281
+ try {
282
+ const records = await resolver.resolveMx(domain);
283
+ return records && records.length > 0 ? 'valid' : 'missing';
284
+ } catch {
285
+ return 'missing';
286
+ }
287
+ }
288
+
289
+ private async checkTxtRecord(
290
+ resolver: plugins.dns.promises.Resolver,
291
+ name: string,
292
+ prefix: string,
293
+ ): Promise<TDnsRecordStatus> {
294
+ try {
295
+ const records = await resolver.resolveTxt(name);
296
+ const flat = records.map((r) => r.join(''));
297
+ const found = flat.some((r) => r.startsWith(prefix));
298
+ return found ? 'valid' : 'missing';
299
+ } catch {
300
+ return 'missing';
301
+ }
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Helpers
306
+ // ---------------------------------------------------------------------------
307
+
308
+ private docToInterface(doc: EmailDomainDoc): IEmailDomain {
309
+ return {
310
+ id: doc.id,
311
+ domain: doc.domain,
312
+ linkedDomainId: doc.linkedDomainId,
313
+ subdomain: doc.subdomain,
314
+ dkim: doc.dkim,
315
+ rateLimits: doc.rateLimits,
316
+ dnsStatus: doc.dnsStatus,
317
+ createdAt: doc.createdAt,
318
+ updatedAt: doc.updatedAt,
319
+ };
320
+ }
321
+ }
@@ -0,0 +1 @@
1
+ export * from './classes.email-domain.manager.js';
@@ -37,6 +37,7 @@ export class OpsServer {
37
37
  private domainHandler!: handlers.DomainHandler;
38
38
  private dnsRecordHandler!: handlers.DnsRecordHandler;
39
39
  private acmeConfigHandler!: handlers.AcmeConfigHandler;
40
+ private emailDomainHandler!: handlers.EmailDomainHandler;
40
41
 
41
42
  constructor(dcRouterRefArg: DcRouter) {
42
43
  this.dcRouterRef = dcRouterRefArg;
@@ -104,6 +105,7 @@ export class OpsServer {
104
105
  this.domainHandler = new handlers.DomainHandler(this);
105
106
  this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
106
107
  this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
108
+ this.emailDomainHandler = new handlers.EmailDomainHandler(this);
107
109
 
108
110
  console.log('✅ OpsServer TypedRequest handlers initialized');
109
111
  }
@@ -0,0 +1,195 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { OpsServer } from '../classes.opsserver.js';
3
+ import * as interfaces from '../../../ts_interfaces/index.js';
4
+
5
+ /**
6
+ * CRUD + DNS provisioning handler for email domains.
7
+ *
8
+ * Auth: admin JWT or API token with `email-domains:read` / `email-domains:write` scope.
9
+ */
10
+ export class EmailDomainHandler {
11
+ public typedrouter = new plugins.typedrequest.TypedRouter();
12
+
13
+ constructor(private opsServerRef: OpsServer) {
14
+ this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
15
+ this.registerHandlers();
16
+ }
17
+
18
+ private async requireAuth(
19
+ request: { identity?: interfaces.data.IIdentity; apiToken?: string },
20
+ requiredScope?: interfaces.data.TApiTokenScope,
21
+ ): Promise<string> {
22
+ if (request.identity?.jwt) {
23
+ try {
24
+ const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
25
+ identity: request.identity,
26
+ });
27
+ if (isAdmin) return request.identity.userId;
28
+ } catch { /* fall through */ }
29
+ }
30
+
31
+ if (request.apiToken) {
32
+ const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
33
+ if (tokenManager) {
34
+ const token = await tokenManager.validateToken(request.apiToken);
35
+ if (token) {
36
+ if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
37
+ return token.createdBy;
38
+ }
39
+ throw new plugins.typedrequest.TypedResponseError('insufficient scope');
40
+ }
41
+ }
42
+ }
43
+
44
+ throw new plugins.typedrequest.TypedResponseError('unauthorized');
45
+ }
46
+
47
+ private get manager() {
48
+ return this.opsServerRef.dcRouterRef.emailDomainManager;
49
+ }
50
+
51
+ private registerHandlers(): void {
52
+ // List all email domains
53
+ this.typedrouter.addTypedHandler(
54
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomains>(
55
+ 'getEmailDomains',
56
+ async (dataArg) => {
57
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
58
+ if (!this.manager) return { domains: [] };
59
+ return { domains: await this.manager.getAll() };
60
+ },
61
+ ),
62
+ );
63
+
64
+ // Get single email domain
65
+ this.typedrouter.addTypedHandler(
66
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomain>(
67
+ 'getEmailDomain',
68
+ async (dataArg) => {
69
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
70
+ if (!this.manager) return { domain: null };
71
+ return { domain: await this.manager.getById(dataArg.id) };
72
+ },
73
+ ),
74
+ );
75
+
76
+ // Create email domain
77
+ this.typedrouter.addTypedHandler(
78
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateEmailDomain>(
79
+ 'createEmailDomain',
80
+ async (dataArg) => {
81
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
82
+ if (!this.manager) {
83
+ return { success: false, message: 'EmailDomainManager not initialized' };
84
+ }
85
+ try {
86
+ const domain = await this.manager.createEmailDomain({
87
+ linkedDomainId: dataArg.linkedDomainId,
88
+ subdomain: dataArg.subdomain,
89
+ dkimSelector: dataArg.dkimSelector,
90
+ dkimKeySize: dataArg.dkimKeySize,
91
+ rotateKeys: dataArg.rotateKeys,
92
+ rotationIntervalDays: dataArg.rotationIntervalDays,
93
+ });
94
+ return { success: true, domain };
95
+ } catch (err: unknown) {
96
+ return { success: false, message: (err as Error).message };
97
+ }
98
+ },
99
+ ),
100
+ );
101
+
102
+ // Update email domain
103
+ this.typedrouter.addTypedHandler(
104
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateEmailDomain>(
105
+ 'updateEmailDomain',
106
+ async (dataArg) => {
107
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
108
+ if (!this.manager) {
109
+ return { success: false, message: 'EmailDomainManager not initialized' };
110
+ }
111
+ try {
112
+ await this.manager.updateEmailDomain(dataArg.id, {
113
+ rotateKeys: dataArg.rotateKeys,
114
+ rotationIntervalDays: dataArg.rotationIntervalDays,
115
+ rateLimits: dataArg.rateLimits,
116
+ });
117
+ return { success: true };
118
+ } catch (err: unknown) {
119
+ return { success: false, message: (err as Error).message };
120
+ }
121
+ },
122
+ ),
123
+ );
124
+
125
+ // Delete email domain
126
+ this.typedrouter.addTypedHandler(
127
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteEmailDomain>(
128
+ 'deleteEmailDomain',
129
+ async (dataArg) => {
130
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
131
+ if (!this.manager) {
132
+ return { success: false, message: 'EmailDomainManager not initialized' };
133
+ }
134
+ try {
135
+ await this.manager.deleteEmailDomain(dataArg.id);
136
+ return { success: true };
137
+ } catch (err: unknown) {
138
+ return { success: false, message: (err as Error).message };
139
+ }
140
+ },
141
+ ),
142
+ );
143
+
144
+ // Validate DNS records
145
+ this.typedrouter.addTypedHandler(
146
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ValidateEmailDomain>(
147
+ 'validateEmailDomain',
148
+ async (dataArg) => {
149
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
150
+ if (!this.manager) {
151
+ return { success: false, message: 'EmailDomainManager not initialized' };
152
+ }
153
+ try {
154
+ const records = await this.manager.validateDns(dataArg.id);
155
+ const domain = await this.manager.getById(dataArg.id);
156
+ return { success: true, domain: domain ?? undefined, records };
157
+ } catch (err: unknown) {
158
+ return { success: false, message: (err as Error).message };
159
+ }
160
+ },
161
+ ),
162
+ );
163
+
164
+ // Get required DNS records
165
+ this.typedrouter.addTypedHandler(
166
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomainDnsRecords>(
167
+ 'getEmailDomainDnsRecords',
168
+ async (dataArg) => {
169
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
170
+ if (!this.manager) return { records: [] };
171
+ return { records: await this.manager.getRequiredDnsRecords(dataArg.id) };
172
+ },
173
+ ),
174
+ );
175
+
176
+ // Auto-provision DNS records
177
+ this.typedrouter.addTypedHandler(
178
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ProvisionEmailDomainDns>(
179
+ 'provisionEmailDomainDns',
180
+ async (dataArg) => {
181
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
182
+ if (!this.manager) {
183
+ return { success: false, message: 'EmailDomainManager not initialized' };
184
+ }
185
+ try {
186
+ const provisioned = await this.manager.provisionDnsRecords(dataArg.id);
187
+ return { success: true, provisioned };
188
+ } catch (err: unknown) {
189
+ return { success: false, message: (err as Error).message };
190
+ }
191
+ },
192
+ ),
193
+ );
194
+ }
195
+ }
@@ -17,4 +17,5 @@ export * from './users.handler.js';
17
17
  export * from './dns-provider.handler.js';
18
18
  export * from './domain.handler.js';
19
19
  export * from './dns-record.handler.js';
20
- export * from './acme-config.handler.js';
20
+ export * from './acme-config.handler.js';
21
+ export * from './email-domain.handler.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.10.0',
6
+ version: '13.12.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }