@serve.zone/dcrouter 16.5.0 → 17.1.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/.smartconfig.json +5 -2
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +6 -1
- package/dist_ts/classes.dcrouter.js +119 -41
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- 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/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -1
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +27 -0
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +110 -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 +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +31 -4
- package/dist_ts/email/index.d.ts +4 -0
- package/dist_ts/email/index.js +5 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- 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/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/opsserver/handlers/stats.handler.js +3 -5
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +2 -35
- package/dist_ts/plugins.js +3 -63
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +8 -0
- package/dist_ts_migrations/index.js +489 -12
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +23 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +136 -44
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/db/documents/classes.smartmta-storage.doc.ts +25 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -0
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +139 -0
- package/ts/email/classes.smartmta-storage-manager.ts +86 -83
- package/ts/email/classes.workapp-mail-manager.ts +47 -4
- package/ts/email/index.ts +4 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/email/smartmta-storage-policy.ts +73 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/opsserver/handlers/stats.handler.ts +2 -4
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +2 -69
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- package/ts_web/elements/overview/ops-view-config.ts +4 -4
|
@@ -10,14 +10,124 @@ import { securityPolicyStatePart, fetchSecurityPolicyAction } from './security.j
|
|
|
10
10
|
import { certificateStatePart, fetchCertificateOverviewAction } from './certificates.js';
|
|
11
11
|
import { remoteIngressStatePart, fetchRemoteIngressAction } from './remoteingress.js';
|
|
12
12
|
import { vpnStatePart, fetchVpnAction } from './vpn.js';
|
|
13
|
-
|
|
13
|
+
import { emailDomainsStatePart, fetchEmailDomainsAction } from './email-domains.js';
|
|
14
|
+
import { emailOpsStatePart, fetchAllEmailsAction } from './email-ops.js';
|
|
15
|
+
import {
|
|
16
|
+
domainsStatePart,
|
|
17
|
+
fetchDnsRecordsForDomainAction,
|
|
18
|
+
fetchDomainsAndProvidersAction,
|
|
19
|
+
} from './domains.js';
|
|
20
|
+
import {
|
|
21
|
+
fetchHttpRedirectsAction,
|
|
22
|
+
fetchMergedRoutesAction,
|
|
23
|
+
routeManagementStatePart,
|
|
24
|
+
} from './routes.js';
|
|
25
|
+
import { opsRealtimeFreshness } from './realtime.js';
|
|
14
26
|
|
|
15
27
|
// ============================================================================
|
|
16
28
|
// TypedSocket Client for Real-time Log Streaming
|
|
17
29
|
// ============================================================================
|
|
18
30
|
|
|
19
31
|
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
|
32
|
+
let socketStatusSubscription: { unsubscribe: () => void } | null = null;
|
|
33
|
+
let socketConnectInFlight = false;
|
|
34
|
+
let socketGeneration = 0;
|
|
20
35
|
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
|
36
|
+
type TRealtimeRefreshGroup =
|
|
37
|
+
| 'emailDomains'
|
|
38
|
+
| 'emailOps'
|
|
39
|
+
| 'dns'
|
|
40
|
+
| 'routes'
|
|
41
|
+
| 'edges'
|
|
42
|
+
| 'certificates';
|
|
43
|
+
|
|
44
|
+
const pendingRealtimeRefreshes = new Set<TRealtimeRefreshGroup>();
|
|
45
|
+
let realtimeRefreshLoopRunning = false;
|
|
46
|
+
|
|
47
|
+
const resourceRefreshGroups: Record<
|
|
48
|
+
interfaces.requests.TOpsRealtimeResource,
|
|
49
|
+
TRealtimeRefreshGroup
|
|
50
|
+
> = {
|
|
51
|
+
emailDomains: 'emailDomains',
|
|
52
|
+
emailQueue: 'emailOps',
|
|
53
|
+
smtpAttempts: 'emailOps',
|
|
54
|
+
dns: 'dns',
|
|
55
|
+
routes: 'routes',
|
|
56
|
+
edges: 'edges',
|
|
57
|
+
certificates: 'certificates',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const refreshRealtimeGroup = async (groupArg: TRealtimeRefreshGroup): Promise<void> => {
|
|
61
|
+
switch (groupArg) {
|
|
62
|
+
case 'emailDomains':
|
|
63
|
+
await emailDomainsStatePart.dispatchAction(fetchEmailDomainsAction, null);
|
|
64
|
+
break;
|
|
65
|
+
case 'emailOps':
|
|
66
|
+
await emailOpsStatePart.dispatchAction(fetchAllEmailsAction, null);
|
|
67
|
+
break;
|
|
68
|
+
case 'dns': {
|
|
69
|
+
const selectedDomainId = domainsStatePart.getState()!.selectedDomainId;
|
|
70
|
+
await domainsStatePart.dispatchAction(fetchDomainsAndProvidersAction, null);
|
|
71
|
+
if (selectedDomainId) {
|
|
72
|
+
await domainsStatePart.dispatchAction(
|
|
73
|
+
fetchDnsRecordsForDomainAction,
|
|
74
|
+
{ domainId: selectedDomainId },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'routes':
|
|
80
|
+
await Promise.all([
|
|
81
|
+
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null),
|
|
82
|
+
routeManagementStatePart.dispatchAction(fetchHttpRedirectsAction, null),
|
|
83
|
+
]);
|
|
84
|
+
break;
|
|
85
|
+
case 'edges':
|
|
86
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
87
|
+
break;
|
|
88
|
+
case 'certificates':
|
|
89
|
+
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const runRealtimeRefreshLoop = async (): Promise<void> => {
|
|
95
|
+
if (realtimeRefreshLoopRunning) return;
|
|
96
|
+
realtimeRefreshLoopRunning = true;
|
|
97
|
+
try {
|
|
98
|
+
while (pendingRealtimeRefreshes.size > 0) {
|
|
99
|
+
const groups = Array.from(pendingRealtimeRefreshes);
|
|
100
|
+
pendingRealtimeRefreshes.clear();
|
|
101
|
+
await Promise.all(
|
|
102
|
+
groups.map(async (group) => {
|
|
103
|
+
try {
|
|
104
|
+
await refreshRealtimeGroup(group);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Realtime resource refresh failed:', group, error);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
realtimeRefreshLoopRunning = false;
|
|
113
|
+
if (pendingRealtimeRefreshes.size > 0) {
|
|
114
|
+
queueMicrotask(() => void runRealtimeRefreshLoop());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const acceptRealtimeInvalidations = (
|
|
120
|
+
serverEpochArg: string,
|
|
121
|
+
invalidationsArg: interfaces.requests.IOpsRealtimeInvalidation[],
|
|
122
|
+
): void => {
|
|
123
|
+
const accepted = opsRealtimeFreshness.accept(serverEpochArg, invalidationsArg);
|
|
124
|
+
for (const invalidation of accepted) {
|
|
125
|
+
pendingRealtimeRefreshes.add(resourceRefreshGroups[invalidation.resource]);
|
|
126
|
+
}
|
|
127
|
+
if (accepted.length > 0) {
|
|
128
|
+
queueMicrotask(() => void runRealtimeRefreshLoop());
|
|
129
|
+
}
|
|
130
|
+
};
|
|
21
131
|
|
|
22
132
|
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
|
|
23
133
|
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
|
|
@@ -51,28 +161,85 @@ socketRouter.addTypedHandler(
|
|
|
51
161
|
)
|
|
52
162
|
);
|
|
53
163
|
|
|
164
|
+
socketRouter.addTypedHandler(
|
|
165
|
+
new plugins.domtools.plugins.typedrequest.TypedHandler<
|
|
166
|
+
interfaces.requests.IReq_PushOpsRealtimeInvalidation
|
|
167
|
+
>(
|
|
168
|
+
'pushOpsRealtimeInvalidation',
|
|
169
|
+
async (dataArg) => {
|
|
170
|
+
acceptRealtimeInvalidations(dataArg.serverEpoch, dataArg.invalidations);
|
|
171
|
+
return {};
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const subscribeSocket = async (
|
|
177
|
+
clientArg: plugins.typedsocket.TypedSocket,
|
|
178
|
+
generationArg: number,
|
|
179
|
+
): Promise<void> => {
|
|
180
|
+
const identity = getActionContext().identity;
|
|
181
|
+
if (!identity || generationArg !== socketGeneration || clientArg !== socketClient) return;
|
|
182
|
+
|
|
183
|
+
const request = clientArg.createTypedRequest<interfaces.requests.IReq_SubscribeOpsRealtime>(
|
|
184
|
+
'subscribeOpsRealtime',
|
|
185
|
+
);
|
|
186
|
+
const response = await request.fire({
|
|
187
|
+
identity,
|
|
188
|
+
...opsRealtimeFreshness.getKnownState(),
|
|
189
|
+
});
|
|
190
|
+
if (generationArg !== socketGeneration || clientArg !== socketClient) return;
|
|
191
|
+
acceptRealtimeInvalidations(response.serverEpoch, response.invalidations);
|
|
192
|
+
};
|
|
193
|
+
|
|
54
194
|
async function connectSocket() {
|
|
55
|
-
if (socketClient) return;
|
|
195
|
+
if (socketClient || socketConnectInFlight || document.hidden) return;
|
|
196
|
+
socketConnectInFlight = true;
|
|
197
|
+
const generation = ++socketGeneration;
|
|
56
198
|
try {
|
|
57
|
-
|
|
199
|
+
const client = await plugins.typedsocket.TypedSocket.createClient(
|
|
58
200
|
socketRouter,
|
|
59
201
|
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
|
|
202
|
+
{ autoReconnect: true },
|
|
60
203
|
);
|
|
61
|
-
|
|
204
|
+
if (generation !== socketGeneration || !loginStatePart.getState()!.isLoggedIn) {
|
|
205
|
+
await client.stop();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
socketClient = client;
|
|
210
|
+
socketStatusSubscription = client.statusSubject.subscribe((status) => {
|
|
211
|
+
if (status === 'connected') {
|
|
212
|
+
void subscribeSocket(client, generation).catch((error) => {
|
|
213
|
+
console.error('Realtime subscription failed after reconnect:', error);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
await subscribeSocket(client, generation);
|
|
62
218
|
} catch (err) {
|
|
63
219
|
console.error('TypedSocket connection failed:', err);
|
|
220
|
+
socketStatusSubscription?.unsubscribe();
|
|
221
|
+
socketStatusSubscription = null;
|
|
222
|
+
if (socketClient) {
|
|
223
|
+
await socketClient.stop().catch(() => undefined);
|
|
224
|
+
}
|
|
64
225
|
socketClient = null;
|
|
226
|
+
} finally {
|
|
227
|
+
socketConnectInFlight = false;
|
|
65
228
|
}
|
|
66
229
|
}
|
|
67
230
|
|
|
68
231
|
async function disconnectSocket() {
|
|
69
|
-
|
|
232
|
+
socketGeneration++;
|
|
233
|
+
socketStatusSubscription?.unsubscribe();
|
|
234
|
+
socketStatusSubscription = null;
|
|
235
|
+
const client = socketClient;
|
|
236
|
+
socketClient = null;
|
|
237
|
+
if (client) {
|
|
70
238
|
try {
|
|
71
|
-
await
|
|
239
|
+
await client.stop();
|
|
72
240
|
} catch {
|
|
73
241
|
// ignore disconnect errors
|
|
74
242
|
}
|
|
75
|
-
socketClient = null;
|
|
76
243
|
}
|
|
77
244
|
}
|
|
78
245
|
|
|
@@ -285,6 +452,8 @@ loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
|
|
|
285
452
|
if (isLoggedIn) {
|
|
286
453
|
connectSocket();
|
|
287
454
|
} else {
|
|
455
|
+
pendingRealtimeRefreshes.clear();
|
|
456
|
+
opsRealtimeFreshness.reset();
|
|
288
457
|
disconnectSocket();
|
|
289
458
|
}
|
|
290
459
|
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './appstate/domains.js';
|
|
|
19
19
|
export * from './appstate/routes.js';
|
|
20
20
|
export * from './appstate/users.js';
|
|
21
21
|
export * from './appstate/email-domains.js';
|
|
22
|
+
export * from './appstate/realtime.js';
|
|
22
23
|
|
|
23
24
|
// Side-effect module: auto-refresh scheduling and TypedSocket log streaming.
|
|
24
25
|
import './appstate/runtime.js';
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
2
|
+
|
|
3
|
+
export interface IEmailDomainCreationProgress {
|
|
4
|
+
percentage: number;
|
|
5
|
+
indeterminate: boolean;
|
|
6
|
+
showPercentage: boolean;
|
|
7
|
+
statusText: string;
|
|
8
|
+
terminalLines: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IEmailDomainCreationProgressInput {
|
|
12
|
+
domain?: interfaces.data.IEmailDomain;
|
|
13
|
+
operation?: interfaces.requests.IEmailDomainActionResult | null;
|
|
14
|
+
requestCompleted: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const settledIntentStatuses = new Set<interfaces.data.TEmailDnsIntentStatus>([
|
|
18
|
+
'satisfied',
|
|
19
|
+
'valid',
|
|
20
|
+
'conflict',
|
|
21
|
+
'failed',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const formatOperationError = (
|
|
25
|
+
errorArg: interfaces.data.IEmailDomainOperationError | interfaces.requests.IEmailDomainActionError,
|
|
26
|
+
): string => {
|
|
27
|
+
const location = errorArg.recordKey ? `${errorArg.recordKey}: ` : '';
|
|
28
|
+
return `${location}${errorArg.message} [${errorArg.code}]`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const appendDnsStatusLines = (
|
|
32
|
+
linesArg: string[],
|
|
33
|
+
dnsStatusArg: interfaces.data.IEmailDomainDnsStatus,
|
|
34
|
+
): void => {
|
|
35
|
+
for (const [label, status] of [
|
|
36
|
+
['MX', dnsStatusArg.mx],
|
|
37
|
+
['SPF', dnsStatusArg.spf],
|
|
38
|
+
['DKIM', dnsStatusArg.dkim],
|
|
39
|
+
['DMARC', dnsStatusArg.dmarc],
|
|
40
|
+
] as const) {
|
|
41
|
+
if (status !== 'unchecked') {
|
|
42
|
+
linesArg.push(`${label}: ${status}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getEmailDomainCreationProgress = (
|
|
48
|
+
inputArg: IEmailDomainCreationProgressInput,
|
|
49
|
+
): IEmailDomainCreationProgress => {
|
|
50
|
+
const { domain, operation, requestCompleted } = inputArg;
|
|
51
|
+
|
|
52
|
+
if (!domain) {
|
|
53
|
+
if (!requestCompleted) {
|
|
54
|
+
return {
|
|
55
|
+
percentage: 0,
|
|
56
|
+
indeterminate: true,
|
|
57
|
+
showPercentage: false,
|
|
58
|
+
statusText: 'Generating DKIM material and creating the domain record...',
|
|
59
|
+
terminalLines: ['Submitting email domain configuration'],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = ['Email domain creation request completed without a persisted domain'];
|
|
64
|
+
for (const error of operation?.errors || []) {
|
|
65
|
+
lines.push(formatOperationError(error));
|
|
66
|
+
}
|
|
67
|
+
if (operation?.message && !lines.includes(operation.message)) {
|
|
68
|
+
lines.push(operation.message);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
percentage: 100,
|
|
72
|
+
indeterminate: false,
|
|
73
|
+
showPercentage: false,
|
|
74
|
+
statusText: operation?.message || 'Email domain creation failed.',
|
|
75
|
+
terminalLines: lines,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const reconciliation = domain.reconciliation;
|
|
80
|
+
const lines = [`Domain record persisted: ${domain.domain}`];
|
|
81
|
+
if (!reconciliation || reconciliation.intents.length === 0) {
|
|
82
|
+
if (!requestCompleted) {
|
|
83
|
+
lines.push('Preparing the managed DNS plan');
|
|
84
|
+
return {
|
|
85
|
+
percentage: 0,
|
|
86
|
+
indeterminate: true,
|
|
87
|
+
showPercentage: false,
|
|
88
|
+
statusText: 'Preparing the managed DNS plan...',
|
|
89
|
+
terminalLines: lines,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push('No DNS reconciliation plan is available');
|
|
94
|
+
const errors = reconciliation?.errors.length
|
|
95
|
+
? reconciliation.errors
|
|
96
|
+
: operation?.errors || [];
|
|
97
|
+
for (const error of errors) {
|
|
98
|
+
lines.push(formatOperationError(error));
|
|
99
|
+
}
|
|
100
|
+
const retryAt = reconciliation?.retryAt || operation?.retryAt;
|
|
101
|
+
if (retryAt) {
|
|
102
|
+
lines.push(`Retry scheduled for ${retryAt}`);
|
|
103
|
+
}
|
|
104
|
+
const statusText =
|
|
105
|
+
reconciliation?.lifecycleStatus === 'failed'
|
|
106
|
+
? operation?.message || `DNS setup failed for ${domain.domain}.`
|
|
107
|
+
: retryAt
|
|
108
|
+
? 'DNS reconciliation is pending and will retry automatically.'
|
|
109
|
+
: operation?.message || 'The domain was created without a DNS reconciliation plan.';
|
|
110
|
+
return {
|
|
111
|
+
percentage: 100,
|
|
112
|
+
indeterminate: false,
|
|
113
|
+
showPercentage: false,
|
|
114
|
+
statusText,
|
|
115
|
+
terminalLines: lines,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const settledCount = reconciliation.intents.filter((intent) =>
|
|
120
|
+
settledIntentStatuses.has(intent.status)
|
|
121
|
+
).length;
|
|
122
|
+
lines.push(`DNS plan prepared: ${reconciliation.intents.length} managed records`);
|
|
123
|
+
lines.push(
|
|
124
|
+
`DNS reconciliation: ${settledCount} settled, ${reconciliation.intents.length - settledCount} pending`,
|
|
125
|
+
);
|
|
126
|
+
appendDnsStatusLines(lines, domain.dnsStatus);
|
|
127
|
+
|
|
128
|
+
if (reconciliation.lifecycleStatus === 'active') {
|
|
129
|
+
lines.push('Email domain is active');
|
|
130
|
+
return {
|
|
131
|
+
percentage: 100,
|
|
132
|
+
indeterminate: false,
|
|
133
|
+
showPercentage: true,
|
|
134
|
+
statusText: `${domain.domain} is active.`,
|
|
135
|
+
terminalLines: lines,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!requestCompleted) {
|
|
140
|
+
return {
|
|
141
|
+
percentage: 0,
|
|
142
|
+
indeterminate: true,
|
|
143
|
+
showPercentage: false,
|
|
144
|
+
statusText: 'Reconciling and validating managed DNS records...',
|
|
145
|
+
terminalLines: lines,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const errors = reconciliation.errors.length > 0
|
|
150
|
+
? reconciliation.errors
|
|
151
|
+
: operation?.errors || [];
|
|
152
|
+
for (const error of errors) {
|
|
153
|
+
lines.push(formatOperationError(error));
|
|
154
|
+
}
|
|
155
|
+
const retryAt = reconciliation.retryAt || operation?.retryAt;
|
|
156
|
+
if (retryAt) {
|
|
157
|
+
lines.push(`Retry scheduled for ${retryAt}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (reconciliation.lifecycleStatus === 'failed') {
|
|
161
|
+
return {
|
|
162
|
+
percentage: 100,
|
|
163
|
+
indeterminate: false,
|
|
164
|
+
showPercentage: false,
|
|
165
|
+
statusText: operation?.message || `DNS setup failed for ${domain.domain}.`,
|
|
166
|
+
terminalLines: lines,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
percentage: 100,
|
|
172
|
+
indeterminate: false,
|
|
173
|
+
showPercentage: false,
|
|
174
|
+
statusText: retryAt
|
|
175
|
+
? `DNS reconciliation is pending and will retry automatically.`
|
|
176
|
+
: `DNS reconciliation is pending for ${domain.domain}.`,
|
|
177
|
+
terminalLines: lines,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const findNewEmailDomain = (
|
|
182
|
+
domainsArg: interfaces.data.IEmailDomain[],
|
|
183
|
+
expectedDomainArg: string,
|
|
184
|
+
existingIdsArg: ReadonlySet<string>,
|
|
185
|
+
): interfaces.data.IEmailDomain | undefined => {
|
|
186
|
+
const expectedDomain = expectedDomainArg.trim().toLowerCase();
|
|
187
|
+
const candidates = domainsArg.filter(
|
|
188
|
+
(domain) => domain.domain.toLowerCase() === expectedDomain && !existingIdsArg.has(domain.id),
|
|
189
|
+
);
|
|
190
|
+
if (candidates.length > 1) {
|
|
191
|
+
throw new Error(`Multiple new email domains appeared for ${expectedDomain}`);
|
|
192
|
+
}
|
|
193
|
+
return candidates[0];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const waitForPollInterval = async (delayMsArg: number, signalArg: AbortSignal): Promise<void> => {
|
|
197
|
+
if (signalArg.aborted) return;
|
|
198
|
+
await new Promise<void>((resolve) => {
|
|
199
|
+
const finish = () => {
|
|
200
|
+
signalArg.removeEventListener('abort', handleAbort);
|
|
201
|
+
resolve();
|
|
202
|
+
};
|
|
203
|
+
const handleAbort = () => {
|
|
204
|
+
clearTimeout(timeoutId);
|
|
205
|
+
finish();
|
|
206
|
+
};
|
|
207
|
+
const timeoutId = setTimeout(finish, delayMsArg);
|
|
208
|
+
signalArg.addEventListener('abort', handleAbort, { once: true });
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export interface IObserveNewEmailDomainOptions {
|
|
213
|
+
expectedDomain: string;
|
|
214
|
+
existingIds: ReadonlySet<string>;
|
|
215
|
+
signal: AbortSignal;
|
|
216
|
+
listDomains: () => Promise<interfaces.data.IEmailDomain[]>;
|
|
217
|
+
getDomain: (idArg: string) => Promise<interfaces.data.IEmailDomain | null>;
|
|
218
|
+
onDomain: (domainArg: interfaces.data.IEmailDomain) => void;
|
|
219
|
+
subscribeToDomains?: (
|
|
220
|
+
listenerArg: (domainsArg: interfaces.data.IEmailDomain[]) => void,
|
|
221
|
+
) => () => void;
|
|
222
|
+
pollIntervalMs?: number;
|
|
223
|
+
timeoutMs?: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const observeNewEmailDomain = async (
|
|
227
|
+
optionsArg: IObserveNewEmailDomainOptions,
|
|
228
|
+
): Promise<interfaces.data.IEmailDomain | undefined> => {
|
|
229
|
+
const startedAt = Date.now();
|
|
230
|
+
const pollIntervalMs = optionsArg.pollIntervalMs ?? (
|
|
231
|
+
optionsArg.subscribeToDomains ? 15_000 : 600
|
|
232
|
+
);
|
|
233
|
+
const timeoutMs = optionsArg.timeoutMs ?? 120_000;
|
|
234
|
+
let observedId: string | undefined;
|
|
235
|
+
let latestDomain: interfaces.data.IEmailDomain | undefined;
|
|
236
|
+
let pushedDomains: interfaces.data.IEmailDomain[] | undefined;
|
|
237
|
+
let wakeObserver: (() => void) | undefined;
|
|
238
|
+
|
|
239
|
+
const unsubscribe = optionsArg.subscribeToDomains?.((domainsArg) => {
|
|
240
|
+
pushedDomains = domainsArg;
|
|
241
|
+
wakeObserver?.();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
while (!optionsArg.signal.aborted && Date.now() - startedAt < timeoutMs) {
|
|
246
|
+
try {
|
|
247
|
+
let domain: interfaces.data.IEmailDomain | null | undefined;
|
|
248
|
+
if (!observedId) {
|
|
249
|
+
const domainSnapshot = pushedDomains || await optionsArg.listDomains();
|
|
250
|
+
pushedDomains = undefined;
|
|
251
|
+
const foundDomain = findNewEmailDomain(
|
|
252
|
+
domainSnapshot,
|
|
253
|
+
optionsArg.expectedDomain,
|
|
254
|
+
optionsArg.existingIds,
|
|
255
|
+
);
|
|
256
|
+
observedId = foundDomain?.id;
|
|
257
|
+
domain = foundDomain;
|
|
258
|
+
} else if (pushedDomains) {
|
|
259
|
+
domain = pushedDomains.find((candidate) => candidate.id === observedId);
|
|
260
|
+
pushedDomains = undefined;
|
|
261
|
+
} else {
|
|
262
|
+
domain = await optionsArg.getDomain(observedId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (optionsArg.signal.aborted) break;
|
|
266
|
+
if (domain) {
|
|
267
|
+
if (domain !== latestDomain) {
|
|
268
|
+
latestDomain = domain;
|
|
269
|
+
optionsArg.onDomain(domain);
|
|
270
|
+
}
|
|
271
|
+
const lifecycleStatus = domain.reconciliation?.lifecycleStatus;
|
|
272
|
+
if (lifecycleStatus === 'active' || lifecycleStatus === 'failed') {
|
|
273
|
+
return domain;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error: unknown) {
|
|
277
|
+
if (error instanceof Error && error.message.startsWith('Multiple new email domains')) {
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
// The realtime subscription or bounded fallback read will retry.
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const updatePromise = new Promise<void>((resolve) => {
|
|
284
|
+
wakeObserver = resolve;
|
|
285
|
+
});
|
|
286
|
+
if (pushedDomains) {
|
|
287
|
+
wakeObserver = undefined;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
await Promise.race([
|
|
291
|
+
updatePromise,
|
|
292
|
+
waitForPollInterval(pollIntervalMs, optionsArg.signal),
|
|
293
|
+
]);
|
|
294
|
+
wakeObserver = undefined;
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
unsubscribe?.();
|
|
298
|
+
wakeObserver?.();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return latestDomain;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export interface IAbortableDeadlineOptions {
|
|
305
|
+
signal?: AbortSignal;
|
|
306
|
+
timeoutMs: number;
|
|
307
|
+
timeoutMessage: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const withAbortableDeadline = async <T>(
|
|
311
|
+
promiseArg: Promise<T>,
|
|
312
|
+
optionsArg: IAbortableDeadlineOptions,
|
|
313
|
+
): Promise<T> => {
|
|
314
|
+
return new Promise<T>((resolve, reject) => {
|
|
315
|
+
let settled = false;
|
|
316
|
+
const finish = (callbackArg: () => void) => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
settled = true;
|
|
319
|
+
clearTimeout(timeoutId);
|
|
320
|
+
optionsArg.signal?.removeEventListener('abort', handleAbort);
|
|
321
|
+
callbackArg();
|
|
322
|
+
};
|
|
323
|
+
const handleAbort = () => {
|
|
324
|
+
finish(() => reject(new Error('Email domain creation progress was interrupted')));
|
|
325
|
+
};
|
|
326
|
+
const timeoutId = setTimeout(() => {
|
|
327
|
+
finish(() => reject(new Error(optionsArg.timeoutMessage)));
|
|
328
|
+
}, optionsArg.timeoutMs);
|
|
329
|
+
|
|
330
|
+
if (optionsArg.signal?.aborted) {
|
|
331
|
+
handleAbort();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
optionsArg.signal?.addEventListener('abort', handleAbort, { once: true });
|
|
335
|
+
promiseArg.then(
|
|
336
|
+
(value) => finish(() => resolve(value)),
|
|
337
|
+
(error) => finish(() => reject(error)),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
};
|