@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
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
IMessageAcceptanceContext,
|
|
5
5
|
IMessageAcceptanceDecision,
|
|
6
6
|
IUnifiedEmailServerOptions,
|
|
7
|
+
IStorageManagerLike,
|
|
7
8
|
} from '@push.rocks/smartmta';
|
|
8
9
|
import type { Buffer } from 'node:buffer';
|
|
9
10
|
import * as plugins from '../plugins.js';
|
|
@@ -158,6 +159,10 @@ export class WorkAppMailManager {
|
|
|
158
159
|
const smtpPassword = shouldRotateSmtpPassword
|
|
159
160
|
? this.generateSmtpPassword()
|
|
160
161
|
: existingIdentity.smtpPassword;
|
|
162
|
+
const requestedSmtpEnabled = request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true;
|
|
163
|
+
const outboundReadiness = requestedSmtpEnabled
|
|
164
|
+
? await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(domain)
|
|
165
|
+
: { ready: false, reason: 'outbound identity disabled', edgeIds: [] };
|
|
161
166
|
const identity: IStoredWorkAppMailIdentity = {
|
|
162
167
|
id: existingIdentity?.id || plugins.smartunique.shortId(),
|
|
163
168
|
externalKey,
|
|
@@ -170,7 +175,7 @@ export class WorkAppMailManager {
|
|
|
170
175
|
inboundTarget: this.resolveNextInboundTarget(request, existingIdentity),
|
|
171
176
|
inbound: this.toLegacyInboundRouteIfSmtpForward(this.resolveNextInboundTarget(request, existingIdentity)),
|
|
172
177
|
smtp: {
|
|
173
|
-
enabled:
|
|
178
|
+
enabled: requestedSmtpEnabled,
|
|
174
179
|
username: existingIdentity?.smtp.username || this.buildSmtpUsername(externalKey),
|
|
175
180
|
},
|
|
176
181
|
createdAt: existingIdentity?.createdAt || now,
|
|
@@ -197,9 +202,12 @@ export class WorkAppMailManager {
|
|
|
197
202
|
identity: this.toPublicIdentity(identity),
|
|
198
203
|
};
|
|
199
204
|
|
|
200
|
-
if (existingIndex < 0 || request.resetSmtpPassword) {
|
|
205
|
+
if ((existingIndex < 0 || request.resetSmtpPassword) && outboundReadiness?.ready) {
|
|
201
206
|
response.smtpCredentials = this.buildSmtpCredentials(identity);
|
|
202
207
|
}
|
|
208
|
+
if (requestedSmtpEnabled && !outboundReadiness?.ready) {
|
|
209
|
+
response.message = `Outbound identity is persisted but remains gated: ${outboundReadiness?.reason || 'email domain is not active'}`;
|
|
210
|
+
}
|
|
203
211
|
|
|
204
212
|
return response;
|
|
205
213
|
}
|
|
@@ -313,6 +321,11 @@ export class WorkAppMailManager {
|
|
|
313
321
|
if (!identity.enabled || !identity.smtp.enabled) {
|
|
314
322
|
return { success: false, message: 'Mail credential is disabled' };
|
|
315
323
|
}
|
|
324
|
+
try {
|
|
325
|
+
await this.assertIdentityOutboundReady(identity);
|
|
326
|
+
} catch (error: unknown) {
|
|
327
|
+
return { success: false, message: (error as Error).message };
|
|
328
|
+
}
|
|
316
329
|
|
|
317
330
|
const now = Date.now();
|
|
318
331
|
identities[identityIndex] = {
|
|
@@ -344,6 +357,11 @@ export class WorkAppMailManager {
|
|
|
344
357
|
if (!identity || !identity.enabled || !identity.smtp.enabled) {
|
|
345
358
|
return this.rejectSenderAuthorization('Managed SMTP credential is not active');
|
|
346
359
|
}
|
|
360
|
+
try {
|
|
361
|
+
await this.assertIdentityOutboundReady(identity);
|
|
362
|
+
} catch (error: unknown) {
|
|
363
|
+
return this.rejectSenderAuthorization((error as Error).message);
|
|
364
|
+
}
|
|
347
365
|
|
|
348
366
|
const envelopeFrom = this.normalizeMailboxAddress(
|
|
349
367
|
context.session.envelope?.mailFrom?.address || context.session.mailFrom || '',
|
|
@@ -373,6 +391,7 @@ export class WorkAppMailManager {
|
|
|
373
391
|
messageArg.from,
|
|
374
392
|
outboundIdentityIdArg,
|
|
375
393
|
);
|
|
394
|
+
await this.assertIdentityOutboundReady(identity);
|
|
376
395
|
const rawMessage = this.buildOutboundRawMessage(identity, messageArg);
|
|
377
396
|
const recipients = this.getMessageRecipients(messageArg);
|
|
378
397
|
const session = this.buildAuthenticatedSession(identity, messageArg.from, recipients, rawMessage);
|
|
@@ -555,7 +574,8 @@ export class WorkAppMailManager {
|
|
|
555
574
|
emailConfig: TConfig,
|
|
556
575
|
): Promise<TConfig> {
|
|
557
576
|
const identities = await this.readStoredIdentities();
|
|
558
|
-
|
|
577
|
+
const readyDomains = await this.getReadyOutboundDomains(identities);
|
|
578
|
+
return this.mergeIdentitiesIntoEmailConfig(emailConfig, identities, readyDomains);
|
|
559
579
|
}
|
|
560
580
|
|
|
561
581
|
public async applyStoredIdentitiesToRuntime(
|
|
@@ -564,10 +584,9 @@ export class WorkAppMailManager {
|
|
|
564
584
|
const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
|
|
565
585
|
if (!emailConfig) return;
|
|
566
586
|
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
);
|
|
587
|
+
const nextIdentities = identities || await this.readStoredIdentities();
|
|
588
|
+
const readyDomains = await this.getReadyOutboundDomains(nextIdentities);
|
|
589
|
+
const nextConfig = this.mergeIdentitiesIntoEmailConfig(emailConfig, nextIdentities, readyDomains);
|
|
571
590
|
|
|
572
591
|
this.dcRouterRef.options.emailConfig = nextConfig;
|
|
573
592
|
if (this.dcRouterRef.emailServer) {
|
|
@@ -593,16 +612,26 @@ export class WorkAppMailManager {
|
|
|
593
612
|
await storageManager.set(this.storageKey, JSON.stringify(state, null, 2));
|
|
594
613
|
}
|
|
595
614
|
|
|
596
|
-
private getRequiredStorageManager()
|
|
597
|
-
|
|
615
|
+
private getRequiredStorageManager(): IStorageManagerLike & Required<
|
|
616
|
+
Pick<IStorageManagerLike, 'get' | 'set'>
|
|
617
|
+
> {
|
|
618
|
+
const storageManager = this.dcRouterRef.storageManager;
|
|
619
|
+
if (
|
|
620
|
+
!storageManager
|
|
621
|
+
|| typeof storageManager.get !== 'function'
|
|
622
|
+
|| typeof storageManager.set !== 'function'
|
|
623
|
+
) {
|
|
598
624
|
throw new Error('DcRouter storage manager does not support managed mail identity persistence');
|
|
599
625
|
}
|
|
600
|
-
return
|
|
626
|
+
return storageManager as IStorageManagerLike & Required<
|
|
627
|
+
Pick<IStorageManagerLike, 'get' | 'set'>
|
|
628
|
+
>;
|
|
601
629
|
}
|
|
602
630
|
|
|
603
631
|
private mergeIdentitiesIntoEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
|
604
632
|
emailConfig: TConfig,
|
|
605
633
|
identities: IStoredWorkAppMailIdentity[],
|
|
634
|
+
readyDomains: Set<string>,
|
|
606
635
|
): TConfig {
|
|
607
636
|
const generatedRoutes = identities
|
|
608
637
|
.filter((identity) => {
|
|
@@ -611,12 +640,12 @@ export class WorkAppMailManager {
|
|
|
611
640
|
})
|
|
612
641
|
.map((identity) => this.buildInboundRoute(identity));
|
|
613
642
|
const generatedOutboundRoutes = identities
|
|
614
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled)
|
|
615
|
-
.map((identity) => this.buildOutboundRoute(identity));
|
|
643
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
644
|
+
.map((identity) => this.buildOutboundRoute(identity, emailConfig));
|
|
616
645
|
const configuredRoutes = (emailConfig.routes || [])
|
|
617
646
|
.filter((route) => !this.isManagedMailRouteName(route.name));
|
|
618
647
|
const generatedUsers = identities
|
|
619
|
-
.filter((identity) => identity.enabled && identity.smtp.enabled)
|
|
648
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled && readyDomains.has(identity.domain))
|
|
620
649
|
.map((identity) => ({
|
|
621
650
|
username: identity.smtp.username,
|
|
622
651
|
password: identity.smtpPassword,
|
|
@@ -679,7 +708,10 @@ export class WorkAppMailManager {
|
|
|
679
708
|
};
|
|
680
709
|
}
|
|
681
710
|
|
|
682
|
-
private buildOutboundRoute(
|
|
711
|
+
private buildOutboundRoute(
|
|
712
|
+
identity: IStoredWorkAppMailIdentity,
|
|
713
|
+
emailConfig: IUnifiedEmailServerOptions,
|
|
714
|
+
): IEmailRoute {
|
|
683
715
|
return {
|
|
684
716
|
name: this.buildOutboundRouteName(identity.externalKey),
|
|
685
717
|
priority: 900,
|
|
@@ -699,7 +731,7 @@ export class WorkAppMailManager {
|
|
|
699
731
|
dkimSign: true,
|
|
700
732
|
dkimOptions: {
|
|
701
733
|
domainName: identity.domain,
|
|
702
|
-
keySelector:
|
|
734
|
+
keySelector: this.getActiveDkimSelector(identity.domain, emailConfig),
|
|
703
735
|
},
|
|
704
736
|
},
|
|
705
737
|
},
|
|
@@ -726,6 +758,40 @@ export class WorkAppMailManager {
|
|
|
726
758
|
await emailDomainManager.ensureEmailDomainForDomainName(domain);
|
|
727
759
|
}
|
|
728
760
|
|
|
761
|
+
private async getReadyOutboundDomains(
|
|
762
|
+
identities: IStoredWorkAppMailIdentity[],
|
|
763
|
+
): Promise<Set<string>> {
|
|
764
|
+
const ready = new Set<string>();
|
|
765
|
+
const requestedDomains = [...new Set(
|
|
766
|
+
identities
|
|
767
|
+
.filter((identity) => identity.enabled && identity.smtp.enabled)
|
|
768
|
+
.map((identity) => identity.domain),
|
|
769
|
+
)];
|
|
770
|
+
for (const domain of requestedDomains) {
|
|
771
|
+
const readiness = await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(domain);
|
|
772
|
+
if (readiness?.ready) ready.add(domain);
|
|
773
|
+
}
|
|
774
|
+
return ready;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private getActiveDkimSelector(
|
|
778
|
+
domain: string,
|
|
779
|
+
emailConfig: IUnifiedEmailServerOptions = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions,
|
|
780
|
+
): string {
|
|
781
|
+
return emailConfig?.domains
|
|
782
|
+
?.find((domainConfig) => domainConfig.domain.toLowerCase() === domain.toLowerCase())
|
|
783
|
+
?.dkim?.selector || 'default';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private async assertIdentityOutboundReady(identity: IStoredWorkAppMailIdentity): Promise<void> {
|
|
787
|
+
const readiness = await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(identity.domain);
|
|
788
|
+
if (!readiness?.ready) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`Outbound mail for ${identity.domain} is gated until DNS and egress identity validation succeeds: ${readiness?.reason || 'EmailDomainManager unavailable'}`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
729
795
|
private normalizeOwnership(
|
|
730
796
|
ownership: interfaces.data.IWorkAppMailOwnership,
|
|
731
797
|
): interfaces.data.IWorkAppMailOwnership {
|
|
@@ -1257,7 +1323,7 @@ export class WorkAppMailManager {
|
|
|
1257
1323
|
allowedFromPatterns: [{ type: 'exactAddress', domain: identityArg.domain, localPart: identityArg.localPart }],
|
|
1258
1324
|
defaultFrom: identityArg.address,
|
|
1259
1325
|
domain: identityArg.domain,
|
|
1260
|
-
dkimSelector:
|
|
1326
|
+
dkimSelector: this.getActiveDkimSelector(identityArg.domain),
|
|
1261
1327
|
credential: this.toMailCredentialPublic(identityArg),
|
|
1262
1328
|
createdAt: identityArg.createdAt,
|
|
1263
1329
|
updatedAt: identityArg.updatedAt,
|
package/ts/email/index.ts
CHANGED
|
@@ -3,7 +3,10 @@ export * from './classes.email-domain.manager.js';
|
|
|
3
3
|
export * from './classes.email-route-builder.js';
|
|
4
4
|
export * from './classes.mail-dns-sync.js';
|
|
5
5
|
export * from './classes.mail-egress-coordinator.js';
|
|
6
|
+
export * from './classes.mail-edge-eligibility.js';
|
|
6
7
|
export * from './classes.email-settings.manager.js';
|
|
8
|
+
export * from './classes.smartmta-blob-storage-manager.js';
|
|
7
9
|
export * from './classes.smartmta-storage-manager.js';
|
|
10
|
+
export * from './smartmta-storage-policy.js';
|
|
8
11
|
export * from './classes.workapp-mail-manager.js';
|
|
9
12
|
export * from './email-dns-records.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface ISmartMtaTextNamespace {
|
|
4
|
+
prefix: string;
|
|
5
|
+
maxBytes: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const smartMtaTextNamespaces: readonly ISmartMtaTextNamespace[] = Object.freeze([
|
|
9
|
+
{ prefix: '/email/dkim/', maxBytes: 128 * 1024 },
|
|
10
|
+
{ prefix: '/email/dns/', maxBytes: 1024 * 1024 },
|
|
11
|
+
{ prefix: '/email/bounces/', maxBytes: 8 * 1024 * 1024 },
|
|
12
|
+
{ prefix: '/email/templates/', maxBytes: 4 * 1024 * 1024 },
|
|
13
|
+
{ prefix: '/email/routes/config.json', maxBytes: 4 * 1024 * 1024 },
|
|
14
|
+
{ prefix: '/security/ip-reputation-cache.json', maxBytes: 8 * 1024 * 1024 },
|
|
15
|
+
{ prefix: '/workhosters/mail-identities.json', maxBytes: 4 * 1024 * 1024 },
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function normalizeSmartMtaStorageKey(key: string): string {
|
|
19
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
20
|
+
throw new Error('SmartMTA storage key must be a non-empty string');
|
|
21
|
+
}
|
|
22
|
+
if (key.includes('\\') || key.includes('\0')) {
|
|
23
|
+
throw new Error(`Invalid SmartMTA storage key: ${key}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const withLeadingSlash = key.startsWith('/') ? key : `/${key}`;
|
|
27
|
+
const segments = withLeadingSlash.split('/').slice(1);
|
|
28
|
+
if (
|
|
29
|
+
segments.length === 0
|
|
30
|
+
|| segments.some((segment, index) => segment === '.' || segment === '..' || (segment === '' && index < segments.length - 1))
|
|
31
|
+
) {
|
|
32
|
+
throw new Error(`Invalid SmartMTA storage key: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalized = `/${segments.filter(Boolean).join('/')}`;
|
|
36
|
+
if (normalized.length > 1024) {
|
|
37
|
+
throw new Error('SmartMTA storage key exceeds 1024 characters');
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getSmartMtaTextNamespace(key: string): ISmartMtaTextNamespace {
|
|
43
|
+
const normalizedKey = normalizeSmartMtaStorageKey(key);
|
|
44
|
+
const namespace = smartMtaTextNamespaces.find((candidate) => {
|
|
45
|
+
return candidate.prefix.endsWith('/')
|
|
46
|
+
? normalizedKey.startsWith(candidate.prefix)
|
|
47
|
+
: normalizedKey === candidate.prefix;
|
|
48
|
+
});
|
|
49
|
+
if (!namespace) {
|
|
50
|
+
throw new Error(`Unsupported SmartMTA text storage namespace: ${normalizedKey}`);
|
|
51
|
+
}
|
|
52
|
+
return namespace;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateSmartMtaTextValue(key: string, value: string): void {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
throw new Error('SmartMTA text storage only accepts string values');
|
|
58
|
+
}
|
|
59
|
+
const namespace = getSmartMtaTextNamespace(key);
|
|
60
|
+
const size = Buffer.byteLength(value, 'utf8');
|
|
61
|
+
if (size > namespace.maxBytes) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`SmartMTA storage value for ${normalizeSmartMtaStorageKey(key)} exceeds ${namespace.maxBytes} bytes`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hashSmartMtaStorageKey(key: string): string {
|
|
69
|
+
return plugins.crypto
|
|
70
|
+
.createHash('sha256')
|
|
71
|
+
.update(normalizeSmartMtaStorageKey(key))
|
|
72
|
+
.digest('hex');
|
|
73
|
+
}
|
|
@@ -3,6 +3,91 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
5
|
|
|
6
|
+
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
|
|
7
|
+
return typeof valueArg === 'object' && valueArg !== null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const normalizeActionErrors = (
|
|
11
|
+
valueArg: unknown,
|
|
12
|
+
): interfaces.requests.IEmailDomainActionError[] | undefined => {
|
|
13
|
+
if (!Array.isArray(valueArg)) return undefined;
|
|
14
|
+
|
|
15
|
+
const errors: interfaces.requests.IEmailDomainActionError[] = [];
|
|
16
|
+
for (const entry of valueArg) {
|
|
17
|
+
if (!isRecord(entry) || typeof entry.message !== 'string') continue;
|
|
18
|
+
errors.push({
|
|
19
|
+
code: typeof entry.code === 'string' ? entry.code : 'EMAIL_DOMAIN_ACTION_FAILED',
|
|
20
|
+
message: entry.message,
|
|
21
|
+
recordKey: typeof entry.recordKey === 'string' ? entry.recordKey : undefined,
|
|
22
|
+
retryable: typeof entry.retryable === 'boolean' ? entry.retryable : undefined,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return errors.length > 0 ? errors : undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const normalizeEmailDomainActionResult = (
|
|
29
|
+
valueArg: unknown,
|
|
30
|
+
defaultSuccess = true,
|
|
31
|
+
): interfaces.requests.IEmailDomainActionResult => {
|
|
32
|
+
const value = isRecord(valueArg) ? valueArg : {};
|
|
33
|
+
const directDomain = isRecord(value.domain)
|
|
34
|
+
? value.domain
|
|
35
|
+
: typeof value.id === 'string' && typeof value.domain === 'string'
|
|
36
|
+
? value
|
|
37
|
+
: undefined;
|
|
38
|
+
const reconciliation = isRecord(directDomain?.reconciliation)
|
|
39
|
+
? directDomain.reconciliation
|
|
40
|
+
: undefined;
|
|
41
|
+
const rawLifecycle = value.lifecycleStatus ?? reconciliation?.lifecycleStatus;
|
|
42
|
+
const lifecycleStatus =
|
|
43
|
+
rawLifecycle === 'active' ||
|
|
44
|
+
rawLifecycle === 'pending' ||
|
|
45
|
+
rawLifecycle === 'failed' ||
|
|
46
|
+
rawLifecycle === 'deleting'
|
|
47
|
+
? rawLifecycle
|
|
48
|
+
: undefined;
|
|
49
|
+
const reconciliationCapability = isRecord(reconciliation?.capability)
|
|
50
|
+
? reconciliation.capability.egressIdentityProof
|
|
51
|
+
: undefined;
|
|
52
|
+
const rawCapability = value.capability ?? reconciliationCapability;
|
|
53
|
+
const capability =
|
|
54
|
+
rawCapability === 'supported' || rawCapability === 'unsupported'
|
|
55
|
+
? rawCapability
|
|
56
|
+
: undefined;
|
|
57
|
+
const errors = normalizeActionErrors(value.errors ?? reconciliation?.errors);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: typeof value.success === 'boolean'
|
|
61
|
+
? value.success
|
|
62
|
+
: lifecycleStatus === 'failed'
|
|
63
|
+
? false
|
|
64
|
+
: defaultSuccess,
|
|
65
|
+
message: typeof value.message === 'string' ? value.message : errors?.[0]?.message,
|
|
66
|
+
lifecycleStatus,
|
|
67
|
+
code: typeof value.code === 'string' ? value.code : errors?.[0]?.code,
|
|
68
|
+
errors,
|
|
69
|
+
retryAt: typeof value.retryAt === 'string'
|
|
70
|
+
? value.retryAt
|
|
71
|
+
: typeof reconciliation?.retryAt === 'string'
|
|
72
|
+
? reconciliation.retryAt
|
|
73
|
+
: undefined,
|
|
74
|
+
capability,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const normalizeEmailDomainActionFailure = (
|
|
79
|
+
errorArg: unknown,
|
|
80
|
+
fallbackMessageArg: string,
|
|
81
|
+
): interfaces.requests.IEmailDomainActionResult => {
|
|
82
|
+
const result = normalizeEmailDomainActionResult(errorArg, false);
|
|
83
|
+
const errorMessage = errorArg instanceof Error ? errorArg.message : undefined;
|
|
84
|
+
return {
|
|
85
|
+
...result,
|
|
86
|
+
success: false,
|
|
87
|
+
message: result.message || errorMessage || result.errors?.[0]?.message || fallbackMessageArg,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
6
91
|
/**
|
|
7
92
|
* CRUD + DNS provisioning handler for email domains.
|
|
8
93
|
*
|
|
@@ -31,6 +116,23 @@ export class EmailDomainHandler {
|
|
|
31
116
|
return this.opsServerRef.dcRouterRef.emailDomainManager;
|
|
32
117
|
}
|
|
33
118
|
|
|
119
|
+
private managerUnavailable(): interfaces.requests.IEmailDomainActionResult {
|
|
120
|
+
const message = 'EmailDomainManager not initialized';
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
message,
|
|
124
|
+
code: 'EMAIL_DOMAIN_MANAGER_UNAVAILABLE',
|
|
125
|
+
capability: 'unsupported',
|
|
126
|
+
errors: [
|
|
127
|
+
{
|
|
128
|
+
code: 'EMAIL_DOMAIN_MANAGER_UNAVAILABLE',
|
|
129
|
+
message,
|
|
130
|
+
retryable: true,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
34
136
|
private registerHandlers(): void {
|
|
35
137
|
// List all email domains
|
|
36
138
|
this.typedrouter.addTypedHandler(
|
|
@@ -63,10 +165,10 @@ export class EmailDomainHandler {
|
|
|
63
165
|
async (dataArg) => {
|
|
64
166
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
65
167
|
if (!this.manager) {
|
|
66
|
-
return
|
|
168
|
+
return this.managerUnavailable();
|
|
67
169
|
}
|
|
68
170
|
try {
|
|
69
|
-
const
|
|
171
|
+
const rawResult: unknown = await this.manager.createEmailDomain({
|
|
70
172
|
linkedDomainId: dataArg.linkedDomainId,
|
|
71
173
|
subdomain: dataArg.subdomain,
|
|
72
174
|
dkimSelector: dataArg.dkimSelector,
|
|
@@ -75,9 +177,18 @@ export class EmailDomainHandler {
|
|
|
75
177
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
76
178
|
remoteIngress: dataArg.remoteIngress,
|
|
77
179
|
});
|
|
78
|
-
|
|
180
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
181
|
+
if (!result.success) return result;
|
|
182
|
+
|
|
183
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
184
|
+
const domain = isRecord(resultRecord?.domain)
|
|
185
|
+
? resultRecord.domain as unknown as interfaces.data.IEmailDomain
|
|
186
|
+
: typeof resultRecord?.id === 'string'
|
|
187
|
+
? rawResult as interfaces.data.IEmailDomain
|
|
188
|
+
: undefined;
|
|
189
|
+
return { ...result, domain };
|
|
79
190
|
} catch (err: unknown) {
|
|
80
|
-
return
|
|
191
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to create email domain');
|
|
81
192
|
}
|
|
82
193
|
},
|
|
83
194
|
),
|
|
@@ -90,18 +201,18 @@ export class EmailDomainHandler {
|
|
|
90
201
|
async (dataArg) => {
|
|
91
202
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
92
203
|
if (!this.manager) {
|
|
93
|
-
return
|
|
204
|
+
return this.managerUnavailable();
|
|
94
205
|
}
|
|
95
206
|
try {
|
|
96
|
-
await this.manager.updateEmailDomain(dataArg.id, {
|
|
207
|
+
const rawResult: unknown = await this.manager.updateEmailDomain(dataArg.id, {
|
|
97
208
|
rotateKeys: dataArg.rotateKeys,
|
|
98
209
|
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
99
210
|
rateLimits: dataArg.rateLimits,
|
|
100
211
|
remoteIngress: dataArg.remoteIngress,
|
|
101
212
|
});
|
|
102
|
-
return
|
|
213
|
+
return normalizeEmailDomainActionResult(rawResult);
|
|
103
214
|
} catch (err: unknown) {
|
|
104
|
-
return
|
|
215
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to update email domain');
|
|
105
216
|
}
|
|
106
217
|
},
|
|
107
218
|
),
|
|
@@ -114,13 +225,13 @@ export class EmailDomainHandler {
|
|
|
114
225
|
async (dataArg) => {
|
|
115
226
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
116
227
|
if (!this.manager) {
|
|
117
|
-
return
|
|
228
|
+
return this.managerUnavailable();
|
|
118
229
|
}
|
|
119
230
|
try {
|
|
120
|
-
await this.manager.deleteEmailDomain(dataArg.id);
|
|
121
|
-
return
|
|
231
|
+
const rawResult: unknown = await this.manager.deleteEmailDomain(dataArg.id);
|
|
232
|
+
return normalizeEmailDomainActionResult(rawResult);
|
|
122
233
|
} catch (err: unknown) {
|
|
123
|
-
return
|
|
234
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to delete email domain');
|
|
124
235
|
}
|
|
125
236
|
},
|
|
126
237
|
),
|
|
@@ -133,14 +244,27 @@ export class EmailDomainHandler {
|
|
|
133
244
|
async (dataArg) => {
|
|
134
245
|
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
135
246
|
if (!this.manager) {
|
|
136
|
-
return
|
|
247
|
+
return this.managerUnavailable();
|
|
137
248
|
}
|
|
138
249
|
try {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
250
|
+
const rawResult: unknown = await this.manager.validateDns(dataArg.id);
|
|
251
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
252
|
+
if (!result.success) return result;
|
|
253
|
+
|
|
254
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
255
|
+
const records = (
|
|
256
|
+
Array.isArray(rawResult)
|
|
257
|
+
? rawResult
|
|
258
|
+
: Array.isArray(resultRecord?.records)
|
|
259
|
+
? resultRecord.records
|
|
260
|
+
: undefined
|
|
261
|
+
) as interfaces.data.IEmailDnsRecord[] | undefined;
|
|
262
|
+
const domain = isRecord(resultRecord?.domain)
|
|
263
|
+
? resultRecord.domain as unknown as interfaces.data.IEmailDomain
|
|
264
|
+
: await this.manager.getById(dataArg.id) ?? undefined;
|
|
265
|
+
return { ...result, domain, records };
|
|
142
266
|
} catch (err: unknown) {
|
|
143
|
-
return
|
|
267
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to validate email domain DNS');
|
|
144
268
|
}
|
|
145
269
|
},
|
|
146
270
|
),
|
|
@@ -165,13 +289,22 @@ export class EmailDomainHandler {
|
|
|
165
289
|
async (dataArg) => {
|
|
166
290
|
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
167
291
|
if (!this.manager) {
|
|
168
|
-
return
|
|
292
|
+
return this.managerUnavailable();
|
|
169
293
|
}
|
|
170
294
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
295
|
+
const rawResult: unknown = await this.manager.provisionDnsRecords(dataArg.id);
|
|
296
|
+
const result = normalizeEmailDomainActionResult(rawResult);
|
|
297
|
+
if (!result.success) return result;
|
|
298
|
+
|
|
299
|
+
const resultRecord = isRecord(rawResult) ? rawResult : undefined;
|
|
300
|
+
const provisioned = typeof rawResult === 'number'
|
|
301
|
+
? rawResult
|
|
302
|
+
: typeof resultRecord?.provisioned === 'number'
|
|
303
|
+
? resultRecord.provisioned
|
|
304
|
+
: undefined;
|
|
305
|
+
return { ...result, provisioned };
|
|
173
306
|
} catch (err: unknown) {
|
|
174
|
-
return
|
|
307
|
+
return normalizeEmailDomainActionFailure(err, 'Failed to provision email domain DNS');
|
|
175
308
|
}
|
|
176
309
|
},
|
|
177
310
|
),
|
|
@@ -562,10 +562,8 @@ export class StatsHandler {
|
|
|
562
562
|
const emailLike = item.processingResult;
|
|
563
563
|
const recipients = Array.isArray(emailLike?.to)
|
|
564
564
|
? emailLike.to
|
|
565
|
-
:
|
|
566
|
-
|
|
567
|
-
: [];
|
|
568
|
-
const subject = emailLike?.subject || emailLike?.email?.subject || '';
|
|
565
|
+
: [];
|
|
566
|
+
const subject = emailLike?.subject || '';
|
|
569
567
|
return {
|
|
570
568
|
id: item.id,
|
|
571
569
|
recipient: recipients[0] || '',
|
package/ts/plugins.ts
CHANGED
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
55
55
|
import * as qenv from '@push.rocks/qenv';
|
|
56
56
|
import * as smartacme from '@push.rocks/smartacme';
|
|
57
|
+
import * as smartbucket from '@push.rocks/smartbucket';
|
|
57
58
|
import * as smartchallenge from '@push.rocks/smartchallenge';
|
|
58
59
|
import * as smartdata from '@push.rocks/smartdata';
|
|
59
60
|
import * as smartdns from '@push.rocks/smartdns';
|
|
@@ -76,7 +77,7 @@ import * as smartrx from '@push.rocks/smartrx';
|
|
|
76
77
|
import * as smartunique from '@push.rocks/smartunique';
|
|
77
78
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
|
78
79
|
|
|
79
|
-
export { projectinfo, qenv, smartacme, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
80
|
+
export { projectinfo, qenv, smartacme, smartbucket, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
80
81
|
|
|
81
82
|
// Define SmartLog types for use in error handling
|
|
82
83
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
|
@@ -223,6 +223,7 @@ export class RemoteIngressHubLifecycle {
|
|
|
223
223
|
tls: tlsConfig,
|
|
224
224
|
performance: manager.getHubPerformanceConfig(),
|
|
225
225
|
});
|
|
226
|
+
tunnelManager.setOnTopologyChanged((reason) => this.dcRouterRef.mailDnsSync.requestSync(reason));
|
|
226
227
|
try {
|
|
227
228
|
await tunnelManager.start();
|
|
228
229
|
} catch (err) {
|