@serve.zone/dcrouter 13.9.2 → 13.11.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 (56) hide show
  1. package/dist_serve/bundle.js +1306 -1180
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  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 +16 -0
  6. package/dist_ts/db/documents/classes.email-domain.doc.js +118 -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 +45 -0
  10. package/dist_ts/email/classes.email-domain.manager.js +272 -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 +149 -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 +68 -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 +140 -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 +2 -2
  28. package/dist_ts_web/appstate.d.ts +20 -0
  29. package/dist_ts_web/appstate.js +81 -1
  30. package/dist_ts_web/elements/domains/ops-view-certificates.js +17 -96
  31. package/dist_ts_web/elements/email/index.d.ts +1 -0
  32. package/dist_ts_web/elements/email/index.js +2 -1
  33. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +19 -0
  34. package/dist_ts_web/elements/email/ops-view-email-domains.js +403 -0
  35. package/dist_ts_web/elements/email/ops-view-email-security.d.ts +1 -1
  36. package/dist_ts_web/elements/email/ops-view-email-security.js +39 -58
  37. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  38. package/dist_ts_web/router.js +2 -2
  39. package/package.json +2 -2
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/classes.dcrouter.ts +17 -0
  42. package/ts/db/documents/classes.email-domain.doc.ts +53 -0
  43. package/ts/db/documents/index.ts +3 -0
  44. package/ts/email/classes.email-domain.manager.ts +316 -0
  45. package/ts/email/index.ts +1 -0
  46. package/ts/opsserver/classes.opsserver.ts +2 -0
  47. package/ts/opsserver/handlers/email-domain.handler.ts +194 -0
  48. package/ts/opsserver/handlers/index.ts +2 -1
  49. package/ts_web/00_commitinfo_data.ts +1 -1
  50. package/ts_web/appstate.ts +123 -0
  51. package/ts_web/elements/domains/ops-view-certificates.ts +16 -95
  52. package/ts_web/elements/email/index.ts +1 -0
  53. package/ts_web/elements/email/ops-view-email-domains.ts +389 -0
  54. package/ts_web/elements/email/ops-view-email-security.ts +38 -57
  55. package/ts_web/elements/ops-dashboard.ts +2 -0
  56. package/ts_web/router.ts +1 -1
@@ -0,0 +1,194 @@
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
+ dkimSelector: dataArg.dkimSelector,
89
+ dkimKeySize: dataArg.dkimKeySize,
90
+ rotateKeys: dataArg.rotateKeys,
91
+ rotationIntervalDays: dataArg.rotationIntervalDays,
92
+ });
93
+ return { success: true, domain };
94
+ } catch (err: unknown) {
95
+ return { success: false, message: (err as Error).message };
96
+ }
97
+ },
98
+ ),
99
+ );
100
+
101
+ // Update email domain
102
+ this.typedrouter.addTypedHandler(
103
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateEmailDomain>(
104
+ 'updateEmailDomain',
105
+ async (dataArg) => {
106
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
107
+ if (!this.manager) {
108
+ return { success: false, message: 'EmailDomainManager not initialized' };
109
+ }
110
+ try {
111
+ await this.manager.updateEmailDomain(dataArg.id, {
112
+ rotateKeys: dataArg.rotateKeys,
113
+ rotationIntervalDays: dataArg.rotationIntervalDays,
114
+ rateLimits: dataArg.rateLimits,
115
+ });
116
+ return { success: true };
117
+ } catch (err: unknown) {
118
+ return { success: false, message: (err as Error).message };
119
+ }
120
+ },
121
+ ),
122
+ );
123
+
124
+ // Delete email domain
125
+ this.typedrouter.addTypedHandler(
126
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteEmailDomain>(
127
+ 'deleteEmailDomain',
128
+ async (dataArg) => {
129
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
130
+ if (!this.manager) {
131
+ return { success: false, message: 'EmailDomainManager not initialized' };
132
+ }
133
+ try {
134
+ await this.manager.deleteEmailDomain(dataArg.id);
135
+ return { success: true };
136
+ } catch (err: unknown) {
137
+ return { success: false, message: (err as Error).message };
138
+ }
139
+ },
140
+ ),
141
+ );
142
+
143
+ // Validate DNS records
144
+ this.typedrouter.addTypedHandler(
145
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ValidateEmailDomain>(
146
+ 'validateEmailDomain',
147
+ async (dataArg) => {
148
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
149
+ if (!this.manager) {
150
+ return { success: false, message: 'EmailDomainManager not initialized' };
151
+ }
152
+ try {
153
+ const records = await this.manager.validateDns(dataArg.id);
154
+ const domain = await this.manager.getById(dataArg.id);
155
+ return { success: true, domain: domain ?? undefined, records };
156
+ } catch (err: unknown) {
157
+ return { success: false, message: (err as Error).message };
158
+ }
159
+ },
160
+ ),
161
+ );
162
+
163
+ // Get required DNS records
164
+ this.typedrouter.addTypedHandler(
165
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomainDnsRecords>(
166
+ 'getEmailDomainDnsRecords',
167
+ async (dataArg) => {
168
+ await this.requireAuth(dataArg, 'email-domains:read' as any);
169
+ if (!this.manager) return { records: [] };
170
+ return { records: await this.manager.getRequiredDnsRecords(dataArg.id) };
171
+ },
172
+ ),
173
+ );
174
+
175
+ // Auto-provision DNS records
176
+ this.typedrouter.addTypedHandler(
177
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ProvisionEmailDomainDns>(
178
+ 'provisionEmailDomainDns',
179
+ async (dataArg) => {
180
+ await this.requireAuth(dataArg, 'email-domains:write' as any);
181
+ if (!this.manager) {
182
+ return { success: false, message: 'EmailDomainManager not initialized' };
183
+ }
184
+ try {
185
+ const provisioned = await this.manager.provisionDnsRecords(dataArg.id);
186
+ return { success: true, provisioned };
187
+ } catch (err: unknown) {
188
+ return { success: false, message: (err as Error).message };
189
+ }
190
+ },
191
+ ),
192
+ );
193
+ }
194
+ }
@@ -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.9.2',
6
+ version: '13.11.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -2377,6 +2377,129 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
2377
2377
  }
2378
2378
  });
2379
2379
 
2380
+ // ============================================================================
2381
+ // Email Domains State
2382
+ // ============================================================================
2383
+
2384
+ export interface IEmailDomainsState {
2385
+ domains: interfaces.data.IEmailDomain[];
2386
+ isLoading: boolean;
2387
+ lastUpdated: number;
2388
+ }
2389
+
2390
+ export const emailDomainsStatePart = await appState.getStatePart<IEmailDomainsState>(
2391
+ 'emailDomains',
2392
+ {
2393
+ domains: [],
2394
+ isLoading: false,
2395
+ lastUpdated: 0,
2396
+ },
2397
+ 'soft',
2398
+ );
2399
+
2400
+ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
2401
+ async (statePartArg): Promise<IEmailDomainsState> => {
2402
+ const context = getActionContext();
2403
+ const currentState = statePartArg.getState()!;
2404
+ if (!context.identity) return currentState;
2405
+
2406
+ try {
2407
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2408
+ interfaces.requests.IReq_GetEmailDomains
2409
+ >('/typedrequest', 'getEmailDomains');
2410
+ const response = await request.fire({ identity: context.identity });
2411
+ return {
2412
+ ...currentState,
2413
+ domains: response.domains,
2414
+ isLoading: false,
2415
+ lastUpdated: Date.now(),
2416
+ };
2417
+ } catch {
2418
+ return { ...currentState, isLoading: false };
2419
+ }
2420
+ },
2421
+ );
2422
+
2423
+ export const createEmailDomainAction = emailDomainsStatePart.createAction<{
2424
+ linkedDomainId: string;
2425
+ dkimSelector?: string;
2426
+ dkimKeySize?: number;
2427
+ rotateKeys?: boolean;
2428
+ rotationIntervalDays?: number;
2429
+ }>(async (statePartArg, args, actionContext) => {
2430
+ const context = getActionContext();
2431
+ const currentState = statePartArg.getState()!;
2432
+ try {
2433
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2434
+ interfaces.requests.IReq_CreateEmailDomain
2435
+ >('/typedrequest', 'createEmailDomain');
2436
+ await request.fire({ identity: context.identity!, ...args });
2437
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2438
+ } catch {
2439
+ return currentState;
2440
+ }
2441
+ });
2442
+
2443
+ export const deleteEmailDomainAction = emailDomainsStatePart.createAction<string>(
2444
+ async (statePartArg, id, actionContext) => {
2445
+ const context = getActionContext();
2446
+ const currentState = statePartArg.getState()!;
2447
+ try {
2448
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2449
+ interfaces.requests.IReq_DeleteEmailDomain
2450
+ >('/typedrequest', 'deleteEmailDomain');
2451
+ await request.fire({ identity: context.identity!, id });
2452
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2453
+ } catch {
2454
+ return currentState;
2455
+ }
2456
+ },
2457
+ );
2458
+
2459
+ export const validateEmailDomainAction = emailDomainsStatePart.createAction<string>(
2460
+ async (statePartArg, id, actionContext) => {
2461
+ const context = getActionContext();
2462
+ const currentState = statePartArg.getState()!;
2463
+ try {
2464
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2465
+ interfaces.requests.IReq_ValidateEmailDomain
2466
+ >('/typedrequest', 'validateEmailDomain');
2467
+ await request.fire({ identity: context.identity!, id });
2468
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2469
+ } catch {
2470
+ return currentState;
2471
+ }
2472
+ },
2473
+ );
2474
+
2475
+ export const provisionEmailDomainDnsAction = emailDomainsStatePart.createAction<string>(
2476
+ async (statePartArg, id, actionContext) => {
2477
+ const context = getActionContext();
2478
+ const currentState = statePartArg.getState()!;
2479
+ try {
2480
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2481
+ interfaces.requests.IReq_ProvisionEmailDomainDns
2482
+ >('/typedrequest', 'provisionEmailDomainDns');
2483
+ await request.fire({ identity: context.identity!, id });
2484
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2485
+ } catch {
2486
+ return currentState;
2487
+ }
2488
+ },
2489
+ );
2490
+
2491
+ // ============================================================================
2492
+ // Email Domain Standalone Functions
2493
+ // ============================================================================
2494
+
2495
+ export async function fetchEmailDomainDnsRecords(id: string) {
2496
+ const context = getActionContext();
2497
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2498
+ interfaces.requests.IReq_GetEmailDomainDnsRecords
2499
+ >('/typedrequest', 'getEmailDomainDnsRecords');
2500
+ return request.fire({ identity: context.identity!, id });
2501
+ }
2502
+
2380
2503
  // ============================================================================
2381
2504
  // TypedSocket Client for Real-time Log Streaming
2382
2505
  // ============================================================================
@@ -54,51 +54,6 @@ export class OpsViewCertificates extends DeesElement {
54
54
  gap: 24px;
55
55
  }
56
56
 
57
- .acmeTileHeader {
58
- display: flex;
59
- justify-content: space-between;
60
- align-items: center;
61
- width: 100%;
62
- padding: 8px 12px;
63
- }
64
-
65
- .acmeTileHeading {
66
- font-size: 14px;
67
- font-weight: 600;
68
- }
69
-
70
- .acmeEmptyContent {
71
- padding: 16px;
72
- font-size: 13px;
73
- line-height: 1.5;
74
- color: ${cssManager.bdTheme('#78350f', '#fde68a')};
75
- }
76
-
77
- .acmeGrid {
78
- display: grid;
79
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
80
- gap: 12px 24px;
81
- padding: 16px;
82
- }
83
-
84
- .acmeField {
85
- display: flex;
86
- flex-direction: column;
87
- gap: 2px;
88
- }
89
-
90
- .acmeLabel {
91
- font-size: 11px;
92
- text-transform: uppercase;
93
- letter-spacing: 0.03em;
94
- color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
95
- }
96
-
97
- .acmeValue {
98
- font-size: 13px;
99
- color: ${cssManager.bdTheme('#111827', '#f3f4f6')};
100
- }
101
-
102
57
  .statusBadge {
103
58
  display: inline-flex;
104
59
  align-items: center;
@@ -227,60 +182,26 @@ export class OpsViewCertificates extends DeesElement {
227
182
 
228
183
  if (!config) {
229
184
  return html`
230
- <dees-tile .heading=${'ACME Settings'}>
231
- <div slot="header" class="acmeTileHeader">
232
- <span class="acmeTileHeading">ACME Settings</span>
233
- <dees-button
234
- @click=${() => this.showEditAcmeDialog()}
235
- .type=${'highlighted'}
236
- >Configure</dees-button>
237
- </div>
238
- <div class="acmeEmptyContent">
239
- No ACME configuration yet. Click <strong>Configure</strong> to set up automated TLS
240
- certificate issuance via Let's Encrypt. You'll also need at least one DNS provider
241
- under <strong>Domains &gt; Providers</strong>.
242
- </div>
243
- </dees-tile>
185
+ <dees-settings
186
+ .heading=${'ACME Settings'}
187
+ .description=${'No ACME configuration yet. Click Configure to set up automated TLS certificate issuance via Let\'s Encrypt. You\'ll also need at least one DNS provider under Domains > Providers.'}
188
+ .actions=${[{ name: 'Configure', action: () => this.showEditAcmeDialog() }]}
189
+ ></dees-settings>
244
190
  `;
245
191
  }
246
192
 
247
193
  return html`
248
- <dees-tile>
249
- <div slot="header" class="acmeTileHeader">
250
- <span class="acmeTileHeading">ACME Settings</span>
251
- <dees-button @click=${() => this.showEditAcmeDialog()}>Edit</dees-button>
252
- </div>
253
- <div class="acmeGrid">
254
- <div class="acmeField">
255
- <span class="acmeLabel">Account email</span>
256
- <span class="acmeValue">${config.accountEmail || '(not set)'}</span>
257
- </div>
258
- <div class="acmeField">
259
- <span class="acmeLabel">Status</span>
260
- <span class="acmeValue">
261
- <span class="statusBadge ${config.enabled ? 'valid' : 'unknown'}">
262
- ${config.enabled ? 'enabled' : 'disabled'}
263
- </span>
264
- </span>
265
- </div>
266
- <div class="acmeField">
267
- <span class="acmeLabel">Mode</span>
268
- <span class="acmeValue">
269
- <span class="statusBadge ${config.useProduction ? 'valid' : 'provisioning'}">
270
- ${config.useProduction ? 'production' : 'staging'}
271
- </span>
272
- </span>
273
- </div>
274
- <div class="acmeField">
275
- <span class="acmeLabel">Auto-renew</span>
276
- <span class="acmeValue">${config.autoRenew ? 'on' : 'off'}</span>
277
- </div>
278
- <div class="acmeField">
279
- <span class="acmeLabel">Renewal threshold</span>
280
- <span class="acmeValue">${config.renewThresholdDays} days</span>
281
- </div>
282
- </div>
283
- </dees-tile>
194
+ <dees-settings
195
+ .heading=${'ACME Settings'}
196
+ .settingsFields=${[
197
+ { key: 'email', label: 'Account email', value: config.accountEmail || '(not set)' },
198
+ { key: 'status', label: 'Status', value: config.enabled ? 'enabled' : 'disabled' },
199
+ { key: 'mode', label: 'Mode', value: config.useProduction ? 'production' : 'staging' },
200
+ { key: 'autoRenew', label: 'Auto-renew', value: config.autoRenew ? 'on' : 'off' },
201
+ { key: 'threshold', label: 'Renewal threshold', value: `${config.renewThresholdDays} days` },
202
+ ]}
203
+ .actions=${[{ name: 'Edit', action: () => this.showEditAcmeDialog() }]}
204
+ ></dees-settings>
284
205
  `;
285
206
  }
286
207
 
@@ -1,2 +1,3 @@
1
1
  export * from './ops-view-emails.js';
2
2
  export * from './ops-view-email-security.js';
3
+ export * from './ops-view-email-domains.js';