@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.
Files changed (82) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +852 -852
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +5 -1
  5. package/dist_ts/classes.dcrouter.js +42 -9
  6. package/dist_ts/db/documents/classes.dns-record.doc.d.ts +4 -0
  7. package/dist_ts/db/documents/classes.dns-record.doc.js +33 -2
  8. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +5 -1
  9. package/dist_ts/db/documents/classes.email-domain.doc.js +26 -2
  10. package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +8 -0
  11. package/dist_ts/db/documents/classes.smartmta-storage.doc.js +91 -0
  12. package/dist_ts/db/documents/index.d.ts +1 -0
  13. package/dist_ts/db/documents/index.js +2 -1
  14. package/dist_ts/dns/manager.dns.d.ts +6 -0
  15. package/dist_ts/dns/manager.dns.js +81 -32
  16. package/dist_ts/dns/providers/cloudflare.provider.d.ts +2 -0
  17. package/dist_ts/dns/providers/cloudflare.provider.js +45 -28
  18. package/dist_ts/email/classes.email-domain.manager.d.ts +43 -38
  19. package/dist_ts/email/classes.email-domain.manager.js +391 -324
  20. package/dist_ts/email/classes.email-settings.manager.js +2 -1
  21. package/dist_ts/email/classes.mail-dns-sync.d.ts +58 -17
  22. package/dist_ts/email/classes.mail-dns-sync.js +857 -141
  23. package/dist_ts/email/classes.mail-edge-eligibility.d.ts +55 -0
  24. package/dist_ts/email/classes.mail-edge-eligibility.js +168 -0
  25. package/dist_ts/email/classes.mail-egress-coordinator.d.ts +3 -1
  26. package/dist_ts/email/classes.mail-egress-coordinator.js +43 -22
  27. package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +27 -0
  28. package/dist_ts/email/classes.smartmta-blob-storage-manager.js +109 -0
  29. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +6 -8
  30. package/dist_ts/email/classes.smartmta-storage-manager.js +63 -86
  31. package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -0
  32. package/dist_ts/email/classes.workapp-mail-manager.js +63 -14
  33. package/dist_ts/email/index.d.ts +3 -0
  34. package/dist_ts/email/index.js +4 -1
  35. package/dist_ts/email/smartmta-storage-policy.d.ts +9 -0
  36. package/dist_ts/email/smartmta-storage-policy.js +58 -0
  37. package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +1 -0
  38. package/dist_ts/opsserver/handlers/email-domain.handler.js +138 -22
  39. package/dist_ts/opsserver/handlers/stats.handler.js +3 -5
  40. package/dist_ts/plugins.d.ts +2 -1
  41. package/dist_ts/plugins.js +3 -2
  42. package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
  43. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
  44. package/dist_ts/remoteingress/classes.tunnel-manager.js +40 -2
  45. package/dist_ts_interfaces/data/dns-record.d.ts +6 -0
  46. package/dist_ts_interfaces/data/email-domain.d.ts +81 -0
  47. package/dist_ts_interfaces/requests/email-domains.d.ts +25 -17
  48. package/dist_ts_migrations/index.d.ts +2 -0
  49. package/dist_ts_migrations/index.js +441 -16
  50. package/dist_ts_web/00_commitinfo_data.js +1 -1
  51. package/dist_ts_web/appstate/email-domains.d.ts +16 -0
  52. package/dist_ts_web/appstate/email-domains.js +140 -19
  53. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +1 -0
  54. package/dist_ts_web/elements/email/ops-view-email-domains.js +36 -13
  55. package/package.json +4 -3
  56. package/readme.md +13 -1
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/classes.dcrouter.ts +55 -11
  59. package/ts/db/documents/classes.dns-record.doc.ts +20 -0
  60. package/ts/db/documents/classes.email-domain.doc.ts +14 -0
  61. package/ts/db/documents/classes.smartmta-storage.doc.ts +25 -0
  62. package/ts/db/documents/index.ts +1 -0
  63. package/ts/dns/manager.dns.ts +89 -29
  64. package/ts/dns/providers/cloudflare.provider.ts +46 -27
  65. package/ts/email/classes.email-domain.manager.ts +416 -368
  66. package/ts/email/classes.email-settings.manager.ts +1 -0
  67. package/ts/email/classes.mail-dns-sync.ts +913 -146
  68. package/ts/email/classes.mail-edge-eligibility.ts +205 -0
  69. package/ts/email/classes.mail-egress-coordinator.ts +46 -23
  70. package/ts/email/classes.smartmta-blob-storage-manager.ts +138 -0
  71. package/ts/email/classes.smartmta-storage-manager.ts +86 -83
  72. package/ts/email/classes.workapp-mail-manager.ts +82 -16
  73. package/ts/email/index.ts +3 -0
  74. package/ts/email/smartmta-storage-policy.ts +73 -0
  75. package/ts/opsserver/handlers/email-domain.handler.ts +154 -21
  76. package/ts/opsserver/handlers/stats.handler.ts +2 -4
  77. package/ts/plugins.ts +2 -1
  78. package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
  79. package/ts/remoteingress/classes.tunnel-manager.ts +53 -7
  80. package/ts_web/00_commitinfo_data.ts +1 -1
  81. package/ts_web/appstate/email-domains.ts +187 -18
  82. 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: request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true,
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
- return this.mergeIdentitiesIntoEmailConfig(emailConfig, identities);
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 nextConfig = this.mergeIdentitiesIntoEmailConfig(
568
- emailConfig,
569
- identities || await this.readStoredIdentities(),
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
- if (!plugins.smartmta.hasStorageManagerMethods(this.dcRouterRef.storageManager, ['get', 'set'])) {
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 this.dcRouterRef.storageManager;
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(identity: IStoredWorkAppMailIdentity): IEmailRoute {
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: 'default',
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: 'default',
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 { success: false, message: 'EmailDomainManager not initialized' };
168
+ return this.managerUnavailable();
67
169
  }
68
170
  try {
69
- const domain = await this.manager.createEmailDomain({
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
- return { success: true, domain };
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 { success: false, message: (err as Error).message };
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 { success: false, message: 'EmailDomainManager not initialized' };
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 { success: true };
213
+ return normalizeEmailDomainActionResult(rawResult);
103
214
  } catch (err: unknown) {
104
- return { success: false, message: (err as Error).message };
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 { success: false, message: 'EmailDomainManager not initialized' };
228
+ return this.managerUnavailable();
118
229
  }
119
230
  try {
120
- await this.manager.deleteEmailDomain(dataArg.id);
121
- return { success: true };
231
+ const rawResult: unknown = await this.manager.deleteEmailDomain(dataArg.id);
232
+ return normalizeEmailDomainActionResult(rawResult);
122
233
  } catch (err: unknown) {
123
- return { success: false, message: (err as Error).message };
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 { success: false, message: 'EmailDomainManager not initialized' };
247
+ return this.managerUnavailable();
137
248
  }
138
249
  try {
139
- const records = await this.manager.validateDns(dataArg.id);
140
- const domain = await this.manager.getById(dataArg.id);
141
- return { success: true, domain: domain ?? undefined, records };
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 { success: false, message: (err as Error).message };
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 { success: false, message: 'EmailDomainManager not initialized' };
292
+ return this.managerUnavailable();
169
293
  }
170
294
  try {
171
- const provisioned = await this.manager.provisionDnsRecords(dataArg.id);
172
- return { success: true, provisioned };
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 { success: false, message: (err as Error).message };
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
- : Array.isArray(emailLike?.email?.to)
566
- ? emailLike.email.to
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) {