@serve.zone/dcrouter 16.4.1 → 16.5.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 (59) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +852 -852
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +2 -1
  5. package/dist_ts/classes.dcrouter.js +20 -4
  6. package/dist_ts/db/documents/classes.dns-record.doc.d.ts +4 -0
  7. package/dist_ts/db/documents/classes.dns-record.doc.js +33 -2
  8. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +5 -1
  9. package/dist_ts/db/documents/classes.email-domain.doc.js +26 -2
  10. package/dist_ts/dns/manager.dns.d.ts +6 -0
  11. package/dist_ts/dns/manager.dns.js +81 -32
  12. package/dist_ts/dns/providers/cloudflare.provider.d.ts +2 -0
  13. package/dist_ts/dns/providers/cloudflare.provider.js +45 -28
  14. package/dist_ts/email/classes.email-domain.manager.d.ts +43 -38
  15. package/dist_ts/email/classes.email-domain.manager.js +391 -324
  16. package/dist_ts/email/classes.mail-dns-sync.d.ts +58 -17
  17. package/dist_ts/email/classes.mail-dns-sync.js +857 -141
  18. package/dist_ts/email/classes.mail-edge-eligibility.d.ts +55 -0
  19. package/dist_ts/email/classes.mail-edge-eligibility.js +168 -0
  20. package/dist_ts/email/classes.mail-egress-coordinator.d.ts +3 -1
  21. package/dist_ts/email/classes.mail-egress-coordinator.js +43 -22
  22. package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -0
  23. package/dist_ts/email/classes.workapp-mail-manager.js +58 -12
  24. package/dist_ts/email/index.d.ts +1 -0
  25. package/dist_ts/email/index.js +2 -1
  26. package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +1 -0
  27. package/dist_ts/opsserver/handlers/email-domain.handler.js +138 -22
  28. package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
  29. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
  30. package/dist_ts/remoteingress/classes.tunnel-manager.js +40 -2
  31. package/dist_ts_interfaces/data/dns-record.d.ts +6 -0
  32. package/dist_ts_interfaces/data/email-domain.d.ts +81 -0
  33. package/dist_ts_interfaces/requests/email-domains.d.ts +25 -17
  34. package/dist_ts_migrations/index.js +152 -9
  35. package/dist_ts_web/00_commitinfo_data.js +1 -1
  36. package/dist_ts_web/appstate/email-domains.d.ts +16 -0
  37. package/dist_ts_web/appstate/email-domains.js +140 -19
  38. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +1 -0
  39. package/dist_ts_web/elements/email/ops-view-email-domains.js +36 -13
  40. package/package.json +3 -3
  41. package/readme.md +13 -1
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/classes.dcrouter.ts +22 -3
  44. package/ts/db/documents/classes.dns-record.doc.ts +20 -0
  45. package/ts/db/documents/classes.email-domain.doc.ts +14 -0
  46. package/ts/dns/manager.dns.ts +89 -29
  47. package/ts/dns/providers/cloudflare.provider.ts +46 -27
  48. package/ts/email/classes.email-domain.manager.ts +416 -368
  49. package/ts/email/classes.mail-dns-sync.ts +913 -146
  50. package/ts/email/classes.mail-edge-eligibility.ts +205 -0
  51. package/ts/email/classes.mail-egress-coordinator.ts +46 -23
  52. package/ts/email/classes.workapp-mail-manager.ts +69 -13
  53. package/ts/email/index.ts +1 -0
  54. package/ts/opsserver/handlers/email-domain.handler.ts +154 -21
  55. package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
  56. package/ts/remoteingress/classes.tunnel-manager.ts +53 -7
  57. package/ts_web/00_commitinfo_data.ts +1 -1
  58. package/ts_web/appstate/email-domains.ts +187 -18
  59. package/ts_web/elements/email/ops-view-email-domains.ts +44 -12
@@ -8,10 +8,26 @@ import { getActionContext } from './login.js';
8
8
  // Email Domains State
9
9
  // ============================================================================
10
10
 
11
+ export type TEmailDomainOperationName =
12
+ | 'create'
13
+ | 'update'
14
+ | 'delete'
15
+ | 'validate'
16
+ | 'provision'
17
+ | 'reconcile'
18
+ | 'rotate';
19
+
20
+ export interface IEmailDomainOperationState extends interfaces.requests.IEmailDomainActionResult {
21
+ operation: TEmailDomainOperationName;
22
+ completedAt: number;
23
+ }
24
+
11
25
  export interface IEmailDomainsState {
12
26
  domains: interfaces.data.IEmailDomain[];
13
27
  settings: interfaces.data.IEmailServerSettings | null;
14
28
  isLoading: boolean;
29
+ error: string | null;
30
+ lastOperation: IEmailDomainOperationState | null;
15
31
  lastUpdated: number;
16
32
  }
17
33
 
@@ -21,11 +37,84 @@ export const emailDomainsStatePart = await appState.getStatePart<IEmailDomainsSt
21
37
  domains: [],
22
38
  settings: null,
23
39
  isLoading: false,
40
+ error: null,
41
+ lastOperation: null,
24
42
  lastUpdated: 0,
25
43
  },
26
44
  'soft',
27
45
  );
28
46
 
47
+ const createOperationState = (
48
+ operationArg: TEmailDomainOperationName,
49
+ responseArg: interfaces.requests.IEmailDomainActionResult,
50
+ ): IEmailDomainOperationState => ({
51
+ operation: operationArg,
52
+ completedAt: Date.now(),
53
+ ...responseArg,
54
+ });
55
+
56
+ const createFailedOperationState = (
57
+ operationArg: TEmailDomainOperationName,
58
+ errorArg: unknown,
59
+ fallbackMessageArg: string,
60
+ ): IEmailDomainOperationState => ({
61
+ operation: operationArg,
62
+ completedAt: Date.now(),
63
+ success: false,
64
+ message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg,
65
+ code: 'EMAIL_DOMAIN_REQUEST_FAILED',
66
+ errors: [
67
+ {
68
+ code: 'EMAIL_DOMAIN_REQUEST_FAILED',
69
+ message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg,
70
+ retryable: true,
71
+ },
72
+ ],
73
+ });
74
+
75
+ export const formatEmailDomainOperationMessage = (
76
+ operationArg: IEmailDomainOperationState,
77
+ fallbackMessageArg: string,
78
+ ): string => {
79
+ const primaryMessage = operationArg.message || fallbackMessageArg;
80
+ const messageParts = [
81
+ operationArg.code ? `${primaryMessage} [${operationArg.code}]` : primaryMessage,
82
+ ];
83
+ for (const error of operationArg.errors || []) {
84
+ const location = error.recordKey ? `${error.recordKey}: ` : '';
85
+ const detail = `${location}${error.message} [${error.code}]`;
86
+ const duplicatesPrimary =
87
+ !error.recordKey &&
88
+ error.message === primaryMessage &&
89
+ error.code === operationArg.code;
90
+ if (!duplicatesPrimary && !messageParts.includes(detail)) {
91
+ messageParts.push(detail);
92
+ }
93
+ }
94
+ if (operationArg.lifecycleStatus && operationArg.lifecycleStatus !== 'active') {
95
+ messageParts.push(`Lifecycle: ${operationArg.lifecycleStatus}`);
96
+ }
97
+ if (operationArg.capability === 'unsupported' && !messageParts.some((part) => part.includes('unsupported'))) {
98
+ messageParts.push('Required capability is unsupported');
99
+ }
100
+ if (operationArg.retryAt) {
101
+ messageParts.push(`Retry scheduled for ${operationArg.retryAt}`);
102
+ }
103
+ return messageParts.join(' — ');
104
+ };
105
+
106
+ const applyOperationState = (
107
+ stateArg: IEmailDomainsState,
108
+ operationArg: IEmailDomainOperationState,
109
+ fallbackMessageArg: string,
110
+ ): IEmailDomainsState => ({
111
+ ...stateArg,
112
+ error: operationArg.success
113
+ ? null
114
+ : formatEmailDomainOperationMessage(operationArg, fallbackMessageArg),
115
+ lastOperation: operationArg,
116
+ });
117
+
29
118
  export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
30
119
  async (statePartArg): Promise<IEmailDomainsState> => {
31
120
  const context = getActionContext();
@@ -46,10 +135,15 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
46
135
  domains: response.domains,
47
136
  settings: settingsResponse.settings,
48
137
  isLoading: false,
138
+ error: null,
49
139
  lastUpdated: Date.now(),
50
140
  };
51
- } catch {
52
- return { ...currentState, isLoading: false };
141
+ } catch (error: unknown) {
142
+ return {
143
+ ...currentState,
144
+ isLoading: false,
145
+ error: error instanceof Error ? error.message : 'Failed to fetch email domains',
146
+ };
53
147
  }
54
148
  },
55
149
  );
@@ -61,17 +155,62 @@ export const createEmailDomainAction = emailDomainsStatePart.createAction<{
61
155
  dkimKeySize?: number;
62
156
  rotateKeys?: boolean;
63
157
  rotationIntervalDays?: number;
158
+ remoteIngress?: interfaces.data.IEmailDomainRemoteIngress;
64
159
  }>(async (statePartArg, args, actionContext) => {
65
160
  const context = getActionContext();
66
161
  const currentState = statePartArg.getState()!;
162
+ if (!context.identity) {
163
+ const operation = createFailedOperationState('create', new Error('Not authenticated'), 'Not authenticated');
164
+ return applyOperationState(currentState, operation, 'Failed to create email domain');
165
+ }
67
166
  try {
68
167
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
69
168
  interfaces.requests.IReq_CreateEmailDomain
70
169
  >('/typedrequest', 'createEmailDomain');
71
- await request.fire({ identity: context.identity!, ...args });
72
- return await actionContext!.dispatch(fetchEmailDomainsAction, null);
73
- } catch {
74
- return currentState;
170
+ const response = await request.fire({ identity: context.identity, ...args });
171
+ const operation = createOperationState('create', response);
172
+ if (!response.success) {
173
+ return applyOperationState(currentState, operation, 'Failed to create email domain');
174
+ }
175
+ const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
176
+ return applyOperationState(refreshedState, operation, 'Email domain created');
177
+ } catch (error: unknown) {
178
+ const operation = createFailedOperationState('create', error, 'Failed to create email domain');
179
+ return applyOperationState(currentState, operation, 'Failed to create email domain');
180
+ }
181
+ });
182
+
183
+ export const updateEmailDomainAction = emailDomainsStatePart.createAction<{
184
+ id: string;
185
+ rotateKeys?: boolean;
186
+ rotationIntervalDays?: number;
187
+ rateLimits?: interfaces.data.IEmailDomain['rateLimits'];
188
+ remoteIngress?: interfaces.data.IEmailDomainRemoteIngress | null;
189
+ }>(async (statePartArg, args, actionContext) => {
190
+ const context = getActionContext();
191
+ const currentState = statePartArg.getState()!;
192
+ if (!context.identity) {
193
+ const operation = createFailedOperationState(
194
+ 'update',
195
+ new Error('Not authenticated'),
196
+ 'Not authenticated',
197
+ );
198
+ return applyOperationState(currentState, operation, 'Failed to update email domain');
199
+ }
200
+ try {
201
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
202
+ interfaces.requests.IReq_UpdateEmailDomain
203
+ >('/typedrequest', 'updateEmailDomain');
204
+ const response = await request.fire({ identity: context.identity, ...args });
205
+ const operation = createOperationState('update', response);
206
+ if (!response.success) {
207
+ return applyOperationState(currentState, operation, 'Failed to update email domain');
208
+ }
209
+ const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
210
+ return applyOperationState(refreshedState, operation, 'Email domain updated');
211
+ } catch (error: unknown) {
212
+ const operation = createFailedOperationState('update', error, 'Failed to update email domain');
213
+ return applyOperationState(currentState, operation, 'Failed to update email domain');
75
214
  }
76
215
  });
77
216
 
@@ -98,14 +237,24 @@ export const deleteEmailDomainAction = emailDomainsStatePart.createAction<string
98
237
  async (statePartArg, id, actionContext) => {
99
238
  const context = getActionContext();
100
239
  const currentState = statePartArg.getState()!;
240
+ if (!context.identity) {
241
+ const operation = createFailedOperationState('delete', new Error('Not authenticated'), 'Not authenticated');
242
+ return applyOperationState(currentState, operation, 'Failed to delete email domain');
243
+ }
101
244
  try {
102
245
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
103
246
  interfaces.requests.IReq_DeleteEmailDomain
104
247
  >('/typedrequest', 'deleteEmailDomain');
105
- await request.fire({ identity: context.identity!, id });
106
- return await actionContext!.dispatch(fetchEmailDomainsAction, null);
107
- } catch {
108
- return currentState;
248
+ const response = await request.fire({ identity: context.identity, id });
249
+ const operation = createOperationState('delete', response);
250
+ if (!response.success) {
251
+ return applyOperationState(currentState, operation, 'Failed to delete email domain');
252
+ }
253
+ const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
254
+ return applyOperationState(refreshedState, operation, 'Email domain deleted');
255
+ } catch (error: unknown) {
256
+ const operation = createFailedOperationState('delete', error, 'Failed to delete email domain');
257
+ return applyOperationState(currentState, operation, 'Failed to delete email domain');
109
258
  }
110
259
  },
111
260
  );
@@ -114,14 +263,24 @@ export const validateEmailDomainAction = emailDomainsStatePart.createAction<stri
114
263
  async (statePartArg, id, actionContext) => {
115
264
  const context = getActionContext();
116
265
  const currentState = statePartArg.getState()!;
266
+ if (!context.identity) {
267
+ const operation = createFailedOperationState('validate', new Error('Not authenticated'), 'Not authenticated');
268
+ return applyOperationState(currentState, operation, 'Failed to validate email domain DNS');
269
+ }
117
270
  try {
118
271
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
119
272
  interfaces.requests.IReq_ValidateEmailDomain
120
273
  >('/typedrequest', 'validateEmailDomain');
121
- await request.fire({ identity: context.identity!, id });
122
- return await actionContext!.dispatch(fetchEmailDomainsAction, null);
123
- } catch {
124
- return currentState;
274
+ const response = await request.fire({ identity: context.identity, id });
275
+ const operation = createOperationState('validate', response);
276
+ if (!response.success) {
277
+ return applyOperationState(currentState, operation, 'Failed to validate email domain DNS');
278
+ }
279
+ const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
280
+ return applyOperationState(refreshedState, operation, 'Email domain DNS validated');
281
+ } catch (error: unknown) {
282
+ const operation = createFailedOperationState('validate', error, 'Failed to validate email domain DNS');
283
+ return applyOperationState(currentState, operation, 'Failed to validate email domain DNS');
125
284
  }
126
285
  },
127
286
  );
@@ -130,14 +289,24 @@ export const provisionEmailDomainDnsAction = emailDomainsStatePart.createAction<
130
289
  async (statePartArg, id, actionContext) => {
131
290
  const context = getActionContext();
132
291
  const currentState = statePartArg.getState()!;
292
+ if (!context.identity) {
293
+ const operation = createFailedOperationState('provision', new Error('Not authenticated'), 'Not authenticated');
294
+ return applyOperationState(currentState, operation, 'Failed to provision email domain DNS');
295
+ }
133
296
  try {
134
297
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
135
298
  interfaces.requests.IReq_ProvisionEmailDomainDns
136
299
  >('/typedrequest', 'provisionEmailDomainDns');
137
- await request.fire({ identity: context.identity!, id });
138
- return await actionContext!.dispatch(fetchEmailDomainsAction, null);
139
- } catch {
140
- return currentState;
300
+ const response = await request.fire({ identity: context.identity, id });
301
+ const operation = createOperationState('provision', response);
302
+ if (!response.success) {
303
+ return applyOperationState(currentState, operation, 'Failed to provision email domain DNS');
304
+ }
305
+ const refreshedState = await actionContext!.dispatch(fetchEmailDomainsAction, null);
306
+ return applyOperationState(refreshedState, operation, 'Email domain DNS provisioned');
307
+ } catch (error: unknown) {
308
+ const operation = createFailedOperationState('provision', error, 'Failed to provision email domain DNS');
309
+ return applyOperationState(currentState, operation, 'Failed to provision email domain DNS');
141
310
  }
142
311
  },
143
312
  );
@@ -218,12 +218,11 @@ export class OpsViewEmailDomains extends DeesElement {
218
218
  type: ['inRow', 'contextmenu'] as any,
219
219
  actionFunc: async (actionData: any) => {
220
220
  const d = actionData.item as interfaces.data.IEmailDomain;
221
- await appstate.emailDomainsStatePart.dispatchAction(
221
+ const nextState = await appstate.emailDomainsStatePart.dispatchAction(
222
222
  appstate.validateEmailDomainAction,
223
223
  d.id,
224
224
  );
225
- const { DeesToast } = await import('@design.estate/dees-catalog');
226
- DeesToast.show({ message: `DNS validated for ${d.domain}`, type: 'success', duration: 2500 });
225
+ await this.showOperationToast(nextState, `DNS validated for ${d.domain}`);
227
226
  },
228
227
  },
229
228
  {
@@ -232,12 +231,11 @@ export class OpsViewEmailDomains extends DeesElement {
232
231
  type: ['inRow', 'contextmenu'] as any,
233
232
  actionFunc: async (actionData: any) => {
234
233
  const d = actionData.item as interfaces.data.IEmailDomain;
235
- await appstate.emailDomainsStatePart.dispatchAction(
234
+ const nextState = await appstate.emailDomainsStatePart.dispatchAction(
236
235
  appstate.provisionEmailDomainDnsAction,
237
236
  d.id,
238
237
  );
239
- const { DeesToast } = await import('@design.estate/dees-catalog');
240
- DeesToast.show({ message: `DNS records provisioned for ${d.domain}`, type: 'success', duration: 2500 });
238
+ await this.showOperationToast(nextState, `DNS records provisioned for ${d.domain}`);
241
239
  },
242
240
  },
243
241
  {
@@ -255,10 +253,11 @@ export class OpsViewEmailDomains extends DeesElement {
255
253
  type: ['inRow', 'contextmenu'] as any,
256
254
  actionFunc: async (actionData: any) => {
257
255
  const d = actionData.item as interfaces.data.IEmailDomain;
258
- await appstate.emailDomainsStatePart.dispatchAction(
256
+ const nextState = await appstate.emailDomainsStatePart.dispatchAction(
259
257
  appstate.deleteEmailDomainAction,
260
258
  d.id,
261
259
  );
260
+ await this.showOperationToast(nextState, `Email domain ${d.domain} deleted`);
262
261
  },
263
262
  },
264
263
  ]}
@@ -282,6 +281,32 @@ export class OpsViewEmailDomains extends DeesElement {
282
281
  return html`<span class="sourceBadge">${label}</span>`;
283
282
  }
284
283
 
284
+ private async showOperationToast(
285
+ stateArg: appstate.IEmailDomainsState,
286
+ successMessageArg: string,
287
+ ): Promise<boolean> {
288
+ const { DeesToast } = await import('@design.estate/dees-catalog');
289
+ const operation = stateArg.lastOperation;
290
+ if (!operation) {
291
+ DeesToast.show({
292
+ message: stateArg.error || 'Email domain operation returned no result',
293
+ type: 'error',
294
+ duration: 5000,
295
+ });
296
+ return false;
297
+ }
298
+
299
+ const isPending = operation.success &&
300
+ operation.lifecycleStatus !== undefined &&
301
+ operation.lifecycleStatus !== 'active';
302
+ DeesToast.show({
303
+ message: appstate.formatEmailDomainOperationMessage(operation, successMessageArg),
304
+ type: operation.success ? (isPending ? 'warning' : 'success') : 'error',
305
+ duration: operation.success && !isPending ? 3000 : 6000,
306
+ });
307
+ return operation.success;
308
+ }
309
+
285
310
  private parsePortList(value: string): number[] {
286
311
  return value
287
312
  .split(',')
@@ -454,7 +479,7 @@ export class OpsViewEmailDomains extends DeesElement {
454
479
  : parseInt(data.dkimKeySize || '2048', 10);
455
480
 
456
481
  const subdomain = data.subdomain?.trim() || undefined;
457
- await appstate.emailDomainsStatePart.dispatchAction(
482
+ const nextState = await appstate.emailDomainsStatePart.dispatchAction(
458
483
  appstate.createEmailDomainAction,
459
484
  {
460
485
  linkedDomainId,
@@ -464,7 +489,11 @@ export class OpsViewEmailDomains extends DeesElement {
464
489
  rotateKeys: Boolean(data.rotateKeys),
465
490
  },
466
491
  );
467
- m.destroy();
492
+ const wasAccepted = await this.showOperationToast(
493
+ nextState,
494
+ `Email domain created`,
495
+ );
496
+ if (wasAccepted) m.destroy();
468
497
  },
469
498
  },
470
499
  ],
@@ -513,12 +542,15 @@ export class OpsViewEmailDomains extends DeesElement {
513
542
  {
514
543
  name: 'Auto-Provision All',
515
544
  action: async (m: any) => {
516
- await appstate.emailDomainsStatePart.dispatchAction(
545
+ const nextState = await appstate.emailDomainsStatePart.dispatchAction(
517
546
  appstate.provisionEmailDomainDnsAction,
518
547
  emailDomain.id,
519
548
  );
520
- DeesToast.show({ message: 'DNS records provisioned', type: 'success', duration: 2500 });
521
- m.destroy();
549
+ const wasAccepted = await this.showOperationToast(
550
+ nextState,
551
+ `DNS records provisioned for ${emailDomain.domain}`,
552
+ );
553
+ if (wasAccepted) m.destroy();
522
554
  },
523
555
  },
524
556
  { name: 'Close', action: async (m: any) => m.destroy() },