@serve.zone/dcrouter 16.4.1 → 17.0.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.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +852 -852
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +5 -1
- package/dist_ts/classes.dcrouter.js +42 -9
- package/dist_ts/db/documents/classes.dns-record.doc.d.ts +4 -0
- package/dist_ts/db/documents/classes.dns-record.doc.js +33 -2
- package/dist_ts/db/documents/classes.email-domain.doc.d.ts +5 -1
- package/dist_ts/db/documents/classes.email-domain.doc.js +26 -2
- package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +8 -0
- package/dist_ts/db/documents/classes.smartmta-storage.doc.js +91 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +6 -0
- package/dist_ts/dns/manager.dns.js +81 -32
- package/dist_ts/dns/providers/cloudflare.provider.d.ts +2 -0
- package/dist_ts/dns/providers/cloudflare.provider.js +45 -28
- package/dist_ts/email/classes.email-domain.manager.d.ts +43 -38
- package/dist_ts/email/classes.email-domain.manager.js +391 -324
- package/dist_ts/email/classes.email-settings.manager.js +2 -1
- package/dist_ts/email/classes.mail-dns-sync.d.ts +58 -17
- package/dist_ts/email/classes.mail-dns-sync.js +857 -141
- package/dist_ts/email/classes.mail-edge-eligibility.d.ts +55 -0
- package/dist_ts/email/classes.mail-edge-eligibility.js +168 -0
- package/dist_ts/email/classes.mail-egress-coordinator.d.ts +3 -1
- package/dist_ts/email/classes.mail-egress-coordinator.js +43 -22
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +27 -0
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +109 -0
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +6 -8
- package/dist_ts/email/classes.smartmta-storage-manager.js +63 -86
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +63 -14
- package/dist_ts/email/index.d.ts +3 -0
- package/dist_ts/email/index.js +4 -1
- package/dist_ts/email/smartmta-storage-policy.d.ts +9 -0
- package/dist_ts/email/smartmta-storage-policy.js +58 -0
- package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-domain.handler.js +138 -22
- package/dist_ts/opsserver/handlers/stats.handler.js +3 -5
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +40 -2
- package/dist_ts_interfaces/data/dns-record.d.ts +6 -0
- package/dist_ts_interfaces/data/email-domain.d.ts +81 -0
- package/dist_ts_interfaces/requests/email-domains.d.ts +25 -17
- package/dist_ts_migrations/index.d.ts +2 -0
- package/dist_ts_migrations/index.js +441 -16
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/email-domains.d.ts +16 -0
- package/dist_ts_web/appstate/email-domains.js +140 -19
- package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +1 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +36 -13
- package/package.json +4 -3
- package/readme.md +13 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +55 -11
- package/ts/db/documents/classes.dns-record.doc.ts +20 -0
- package/ts/db/documents/classes.email-domain.doc.ts +14 -0
- package/ts/db/documents/classes.smartmta-storage.doc.ts +25 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/manager.dns.ts +89 -29
- package/ts/dns/providers/cloudflare.provider.ts +46 -27
- package/ts/email/classes.email-domain.manager.ts +416 -368
- package/ts/email/classes.email-settings.manager.ts +1 -0
- package/ts/email/classes.mail-dns-sync.ts +913 -146
- package/ts/email/classes.mail-edge-eligibility.ts +205 -0
- package/ts/email/classes.mail-egress-coordinator.ts +46 -23
- package/ts/email/classes.smartmta-blob-storage-manager.ts +138 -0
- package/ts/email/classes.smartmta-storage-manager.ts +86 -83
- package/ts/email/classes.workapp-mail-manager.ts +82 -16
- package/ts/email/index.ts +3 -0
- package/ts/email/smartmta-storage-policy.ts +73 -0
- package/ts/opsserver/handlers/email-domain.handler.ts +154 -21
- package/ts/opsserver/handlers/stats.handler.ts +2 -4
- package/ts/plugins.ts +2 -1
- package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +53 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/email-domains.ts +187 -18
- package/ts_web/elements/email/ops-view-email-domains.ts +44 -12
|
@@ -20,8 +20,14 @@ export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgres
|
|
|
20
20
|
edgeFilter?: string[];
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
export type TRemoteIngressHubStatus = Awaited<ReturnType<
|
|
24
|
+
InstanceType<typeof plugins.remoteingress.RemoteIngressHub>['getStatus']
|
|
25
|
+
>>;
|
|
26
|
+
type TRemoteIngressHubEdgeStatus = TRemoteIngressHubStatus['connectedEdges'][number];
|
|
27
|
+
|
|
23
28
|
/** Cooldown applied to an edge after a failed egress open before it is deprioritized. */
|
|
24
29
|
const egressFailureCooldownMs = 30_000;
|
|
30
|
+
const egressStatusFreshnessMs = 45_000;
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
@@ -38,6 +44,7 @@ export class TunnelManager {
|
|
|
38
44
|
private egressProxyEdgeByProxyId: Map<string, string> = new Map();
|
|
39
45
|
private activeEgressProxiesByEdge: Map<string, number> = new Map();
|
|
40
46
|
private egressFailureCooldownUntil: Map<string, number> = new Map();
|
|
47
|
+
private onTopologyChanged?: (reason: string) => void;
|
|
41
48
|
|
|
42
49
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
43
50
|
this.manager = manager;
|
|
@@ -54,10 +61,12 @@ export class TunnelManager {
|
|
|
54
61
|
lastHeartbeat: Date.now(),
|
|
55
62
|
connectedAt: Date.now(),
|
|
56
63
|
});
|
|
64
|
+
this.onTopologyChanged?.(`RemoteIngress edge connected: ${data.edgeId}`);
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
|
60
68
|
this.edgeStatuses.delete(data.edgeId);
|
|
69
|
+
this.onTopologyChanged?.(`RemoteIngress edge disconnected: ${data.edgeId}`);
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
this.hub.on('streamSummary', (data: {
|
|
@@ -94,6 +103,14 @@ export class TunnelManager {
|
|
|
94
103
|
this.egressFailureCooldownUntil.set(data.edgeId, Date.now() + egressFailureCooldownMs);
|
|
95
104
|
}
|
|
96
105
|
});
|
|
106
|
+
|
|
107
|
+
this.hub.on('egressIdentityChanged', (data) => {
|
|
108
|
+
this.onTopologyChanged?.(`RemoteIngress egress identity changed: ${data.edgeId}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public setOnTopologyChanged(callback: (reason: string) => void): void {
|
|
113
|
+
this.onTopologyChanged = callback;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
/**
|
|
@@ -165,6 +182,7 @@ export class TunnelManager {
|
|
|
165
182
|
if (this.stopped) return;
|
|
166
183
|
if (!hubStatus || !hubStatus.connectedEdges) return;
|
|
167
184
|
|
|
185
|
+
const before = this.getTopologySignature();
|
|
168
186
|
const rustEdgeIds = new Set<string>();
|
|
169
187
|
|
|
170
188
|
for (const rustEdge of hubStatus.connectedEdges) {
|
|
@@ -199,6 +217,10 @@ export class TunnelManager {
|
|
|
199
217
|
this.edgeStatuses.delete(edgeId);
|
|
200
218
|
}
|
|
201
219
|
}
|
|
220
|
+
const after = this.getTopologySignature();
|
|
221
|
+
if (before !== after) {
|
|
222
|
+
this.onTopologyChanged?.('RemoteIngress mail egress topology changed');
|
|
223
|
+
}
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
/**
|
|
@@ -260,33 +282,43 @@ export class TunnelManager {
|
|
|
260
282
|
);
|
|
261
283
|
}
|
|
262
284
|
|
|
285
|
+
/** Authoritative hub state used by the source-bound mail identity adapter. */
|
|
286
|
+
public async getAuthoritativeHubStatus(): Promise<TRemoteIngressHubStatus | undefined> {
|
|
287
|
+
if (this.stopped) return undefined;
|
|
288
|
+
return await this.hub.getStatus();
|
|
289
|
+
}
|
|
290
|
+
|
|
263
291
|
/**
|
|
264
292
|
* Eligibility (connected + egressEnabled + egressTcpV1 + native QUIC), scoped by
|
|
265
293
|
* edgeId/edgeFilter, capped by per-edge maxConcurrentStreams, ordered deterministically:
|
|
266
294
|
* not-in-cooldown first, then fewest active tunnels, then edgeId.
|
|
267
295
|
*/
|
|
268
296
|
private selectEgressCandidates(
|
|
269
|
-
connectedEdges:
|
|
297
|
+
connectedEdges: TRemoteIngressHubEdgeStatus[],
|
|
270
298
|
request: TRemoteIngressEgressProxyRequest,
|
|
271
299
|
): Array<{ edgeId: string; activeStreams: number }> {
|
|
272
300
|
const eligible = connectedEdges.filter((edge: any) => {
|
|
273
301
|
const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
|
|
274
302
|
const usesQuic = edge.transportMode === 'quic'
|
|
275
303
|
|| (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
|
|
276
|
-
|
|
304
|
+
const observed = this.edgeStatuses.get(edge.edgeId);
|
|
305
|
+
const fresh = observed?.connected
|
|
306
|
+
&& observed.lastHeartbeat !== null
|
|
307
|
+
&& Date.now() - observed.lastHeartbeat <= egressStatusFreshnessMs;
|
|
308
|
+
return edge.egressEnabled === true && hasEgressCapability && usesQuic && fresh;
|
|
277
309
|
});
|
|
278
310
|
|
|
279
311
|
let scoped = eligible;
|
|
280
312
|
if (request.edgeId) {
|
|
281
|
-
scoped = eligible.filter((edge
|
|
313
|
+
scoped = eligible.filter((edge) => edge.edgeId === request.edgeId);
|
|
282
314
|
} else if (request.edgeFilter && request.edgeFilter.length > 0) {
|
|
283
315
|
const allowedIds = new Set(
|
|
284
316
|
this.manager.resolveEdgesByFilter(request.edgeFilter).map((edge) => edge.id),
|
|
285
317
|
);
|
|
286
|
-
scoped = eligible.filter((edge
|
|
318
|
+
scoped = eligible.filter((edge) => allowedIds.has(edge.edgeId));
|
|
287
319
|
}
|
|
288
320
|
|
|
289
|
-
const underCap = scoped.filter((edge
|
|
321
|
+
const underCap = scoped.filter((edge) => {
|
|
290
322
|
const cap = this.manager.getEdge(edge.edgeId)?.egress?.maxConcurrentStreams;
|
|
291
323
|
if (!cap) return true;
|
|
292
324
|
return (this.activeEgressProxiesByEdge.get(edge.edgeId) || 0) < cap;
|
|
@@ -295,7 +327,7 @@ export class TunnelManager {
|
|
|
295
327
|
const now = Date.now();
|
|
296
328
|
const inCooldown = (edgeId: string) => (this.egressFailureCooldownUntil.get(edgeId) || 0) > now;
|
|
297
329
|
return underCap
|
|
298
|
-
.map((edge
|
|
330
|
+
.map((edge) => ({ edgeId: edge.edgeId, activeStreams: Number(edge.activeStreams) || 0 }))
|
|
299
331
|
.sort((a, b) => {
|
|
300
332
|
const cooldownDelta = Number(inCooldown(a.edgeId)) - Number(inCooldown(b.edgeId));
|
|
301
333
|
if (cooldownDelta !== 0) return cooldownDelta;
|
|
@@ -355,7 +387,7 @@ export class TunnelManager {
|
|
|
355
387
|
return await this.hub.stopEgressTcpProxy(proxyId);
|
|
356
388
|
}
|
|
357
389
|
|
|
358
|
-
private applyRustStatus(status: IRemoteIngressStatus, rustEdge:
|
|
390
|
+
private applyRustStatus(status: IRemoteIngressStatus, rustEdge: TRemoteIngressHubEdgeStatus): void {
|
|
359
391
|
status.transportMode = rustEdge.transportMode;
|
|
360
392
|
status.fallbackUsed = rustEdge.fallbackUsed;
|
|
361
393
|
status.capabilities = rustEdge.capabilities;
|
|
@@ -367,6 +399,20 @@ export class TunnelManager {
|
|
|
367
399
|
status.udp = rustEdge.udp;
|
|
368
400
|
}
|
|
369
401
|
|
|
402
|
+
private getTopologySignature(): string {
|
|
403
|
+
return [...this.edgeStatuses.values()]
|
|
404
|
+
.map((status) => [
|
|
405
|
+
status.edgeId,
|
|
406
|
+
status.connected,
|
|
407
|
+
status.egressEnabled,
|
|
408
|
+
status.transportMode,
|
|
409
|
+
status.fallbackUsed,
|
|
410
|
+
[...(status.capabilities || [])].sort().join(','),
|
|
411
|
+
].join('|'))
|
|
412
|
+
.sort()
|
|
413
|
+
.join(';');
|
|
414
|
+
}
|
|
415
|
+
|
|
370
416
|
/**
|
|
371
417
|
* Get runtime statuses for all known edges.
|
|
372
418
|
*/
|
|
@@ -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 {
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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() },
|