@serve.zone/dcrouter 13.24.0 → 13.26.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 (81) hide show
  1. package/.smartconfig.json +3 -11
  2. package/dist_serve/bundle.js +4046 -3552
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +2 -1
  5. package/dist_ts/classes.dcrouter.js +6 -4
  6. package/dist_ts/config/classes.api-token-manager.d.ts +2 -2
  7. package/dist_ts/config/classes.api-token-manager.js +31 -3
  8. package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
  9. package/dist_ts/config/classes.route-config-manager.js +38 -1
  10. package/dist_ts/db/documents/classes.api-token.doc.d.ts +2 -1
  11. package/dist_ts/db/documents/classes.api-token.doc.js +8 -2
  12. package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +1 -0
  13. package/dist_ts/db/documents/classes.ip-intelligence.doc.js +8 -2
  14. package/dist_ts/email/classes.email-domain.manager.d.ts +4 -1
  15. package/dist_ts/email/classes.email-domain.manager.js +30 -1
  16. package/dist_ts/email/classes.workapp-mail-manager.d.ts +35 -0
  17. package/dist_ts/email/classes.workapp-mail-manager.js +273 -0
  18. package/dist_ts/email/index.d.ts +1 -0
  19. package/dist_ts/email/index.js +2 -1
  20. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  21. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  22. package/dist_ts/opsserver/handlers/admin.handler.js +9 -4
  23. package/dist_ts/opsserver/handlers/api-token.handler.js +2 -2
  24. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +4 -0
  25. package/dist_ts/opsserver/handlers/certificate.handler.js +41 -11
  26. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  27. package/dist_ts/opsserver/handlers/index.js +2 -1
  28. package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +26 -0
  29. package/dist_ts/opsserver/handlers/workhoster.handler.js +402 -0
  30. package/dist_ts/security/classes.security-policy-manager.d.ts +8 -2
  31. package/dist_ts/security/classes.security-policy-manager.js +83 -7
  32. package/dist_ts_apiclient/classes.dcrouterapiclient.d.ts +2 -0
  33. package/dist_ts_apiclient/classes.dcrouterapiclient.js +4 -1
  34. package/dist_ts_apiclient/classes.workhoster.d.ts +14 -0
  35. package/dist_ts_apiclient/classes.workhoster.js +29 -0
  36. package/dist_ts_apiclient/index.d.ts +1 -0
  37. package/dist_ts_apiclient/index.js +2 -1
  38. package/dist_ts_interfaces/data/index.d.ts +1 -0
  39. package/dist_ts_interfaces/data/index.js +2 -1
  40. package/dist_ts_interfaces/data/route-management.d.ts +38 -1
  41. package/dist_ts_interfaces/data/workhoster.d.ts +131 -0
  42. package/dist_ts_interfaces/data/workhoster.js +2 -0
  43. package/dist_ts_interfaces/requests/api-tokens.d.ts +2 -1
  44. package/dist_ts_interfaces/requests/certificate.d.ts +12 -6
  45. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  46. package/dist_ts_interfaces/requests/index.js +2 -1
  47. package/dist_ts_interfaces/requests/workhoster.d.ts +98 -0
  48. package/dist_ts_interfaces/requests/workhoster.js +2 -0
  49. package/dist_ts_migrations/index.js +8 -2
  50. package/dist_ts_web/00_commitinfo_data.js +1 -1
  51. package/dist_ts_web/appstate.d.ts +1 -1
  52. package/dist_ts_web/appstate.js +3 -2
  53. package/dist_ts_web/elements/access/ops-view-apitokens.d.ts +1 -0
  54. package/dist_ts_web/elements/access/ops-view-apitokens.js +58 -3
  55. package/package.json +24 -23
  56. package/readme.md +108 -128
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/classes.dcrouter.ts +5 -3
  59. package/ts/config/classes.api-token-manager.ts +32 -1
  60. package/ts/config/classes.route-config-manager.ts +37 -0
  61. package/ts/db/documents/classes.api-token.doc.ts +4 -1
  62. package/ts/db/documents/classes.ip-intelligence.doc.ts +3 -0
  63. package/ts/email/classes.email-domain.manager.ts +33 -1
  64. package/ts/email/classes.workapp-mail-manager.ts +343 -0
  65. package/ts/email/index.ts +1 -0
  66. package/ts/opsserver/classes.opsserver.ts +3 -1
  67. package/ts/opsserver/handlers/admin.handler.ts +11 -4
  68. package/ts/opsserver/handlers/api-token.handler.ts +1 -0
  69. package/ts/opsserver/handlers/certificate.handler.ts +45 -12
  70. package/ts/opsserver/handlers/index.ts +2 -1
  71. package/ts/opsserver/handlers/workhoster.handler.ts +490 -0
  72. package/ts/readme.md +2 -2
  73. package/ts/security/classes.security-policy-manager.ts +90 -8
  74. package/ts_apiclient/classes.dcrouterapiclient.ts +3 -0
  75. package/ts_apiclient/classes.workhoster.ts +49 -0
  76. package/ts_apiclient/index.ts +1 -0
  77. package/ts_apiclient/readme.md +54 -44
  78. package/ts_web/00_commitinfo_data.ts +1 -1
  79. package/ts_web/appstate.ts +7 -1
  80. package/ts_web/elements/access/ops-view-apitokens.ts +58 -3
  81. package/ts_web/readme.md +36 -19
@@ -31,7 +31,7 @@ import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyMana
31
31
  import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
32
32
  import { DnsManager } from './dns/manager.dns.js';
33
33
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
34
- import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
34
+ import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
35
35
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
36
36
  import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
37
37
 
@@ -285,6 +285,7 @@ export class DcRouter {
285
285
  // ACME configuration (DB-backed singleton, replaces tls.contactEmail)
286
286
  public acmeConfigManager?: AcmeConfigManager;
287
287
  public emailDomainManager?: EmailDomainManager;
288
+ public workAppMailManager: WorkAppMailManager;
288
289
  public securityPolicyManager?: SecurityPolicyManager;
289
290
 
290
291
  // Auto-discovered public IP (populated by generateAuthoritativeRecords)
@@ -339,6 +340,7 @@ export class DcRouter {
339
340
  this.storageManager = new SmartMtaStorageManager(
340
341
  plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage')
341
342
  );
343
+ this.workAppMailManager = new WorkAppMailManager(this);
342
344
 
343
345
  // Initialize service manager and register all services
344
346
  this.serviceManager = new plugins.taskbuffer.ServiceManager({
@@ -1630,7 +1632,7 @@ export class DcRouter {
1630
1632
  }
1631
1633
 
1632
1634
  // Create config with mapped ports
1633
- const emailConfig: IUnifiedEmailServerOptions = {
1635
+ const emailConfig: IUnifiedEmailServerOptions = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
1634
1636
  ...this.options.emailConfig,
1635
1637
  domains: transformedDomains,
1636
1638
  ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
@@ -1640,7 +1642,7 @@ export class DcRouter {
1640
1642
  persistentPath: plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-queue'),
1641
1643
  ...this.options.emailConfig.queue,
1642
1644
  },
1643
- };
1645
+ });
1644
1646
 
1645
1647
  // Create unified email server
1646
1648
  this.emailServer = new UnifiedEmailServer(this, emailConfig);
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import { ApiTokenDoc } from '../db/index.js';
4
4
  import type {
5
+ IApiTokenPolicy,
5
6
  IStoredApiToken,
6
7
  IApiTokenInfo,
7
8
  TApiTokenScope,
@@ -33,6 +34,7 @@ export class ApiTokenManager {
33
34
  scopes: TApiTokenScope[],
34
35
  expiresInDays: number | null,
35
36
  createdBy: string,
37
+ policy?: IApiTokenPolicy,
36
38
  ): Promise<{ id: string; rawToken: string }> {
37
39
  const id = plugins.uuid.v4();
38
40
  const randomBytes = plugins.crypto.randomBytes(32);
@@ -47,6 +49,7 @@ export class ApiTokenManager {
47
49
  name,
48
50
  tokenHash,
49
51
  scopes,
52
+ policy,
50
53
  createdAt: now,
51
54
  expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
52
55
  lastUsedAt: null,
@@ -87,7 +90,31 @@ export class ApiTokenManager {
87
90
  * Check if a token has a specific scope.
88
91
  */
89
92
  public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
90
- return token.scopes.includes(scope);
93
+ if (token.policy?.role === 'admin') return true;
94
+
95
+ const isGatewayClientToken = token.policy?.role === 'gatewayClient';
96
+ const gatewayClientAllowedScopes = new Set<TApiTokenScope>([
97
+ 'gateway-clients:read',
98
+ 'gateway-clients:write',
99
+ 'workhosters:read',
100
+ 'workhosters:write',
101
+ ]);
102
+ if (isGatewayClientToken && !gatewayClientAllowedScopes.has(scope)) {
103
+ return false;
104
+ }
105
+
106
+ if (!isGatewayClientToken && token.scopes.includes('*')) return true;
107
+
108
+ const scopes = new Set<TApiTokenScope>([...token.scopes, ...(token.policy?.scopes || [])]);
109
+ if (scopes.has(scope)) return true;
110
+
111
+ const compatibilityAliases: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
112
+ 'gateway-clients:read': ['workhosters:read'],
113
+ 'gateway-clients:write': ['workhosters:write'],
114
+ 'workhosters:read': ['gateway-clients:read'],
115
+ 'workhosters:write': ['gateway-clients:write'],
116
+ };
117
+ return Boolean(compatibilityAliases[scope]?.some((alias) => scopes.has(alias)));
91
118
  }
92
119
 
93
120
  /**
@@ -100,6 +127,7 @@ export class ApiTokenManager {
100
127
  id: stored.id,
101
128
  name: stored.name,
102
129
  scopes: stored.scopes,
130
+ policy: stored.policy,
103
131
  createdAt: stored.createdAt,
104
132
  expiresAt: stored.expiresAt,
105
133
  lastUsedAt: stored.lastUsedAt,
@@ -165,6 +193,7 @@ export class ApiTokenManager {
165
193
  name: doc.name,
166
194
  tokenHash: doc.tokenHash,
167
195
  scopes: doc.scopes,
196
+ policy: doc.policy,
168
197
  createdAt: doc.createdAt,
169
198
  expiresAt: doc.expiresAt,
170
199
  lastUsedAt: doc.lastUsedAt,
@@ -181,6 +210,7 @@ export class ApiTokenManager {
181
210
  existing.name = stored.name;
182
211
  existing.tokenHash = stored.tokenHash;
183
212
  existing.scopes = stored.scopes;
213
+ existing.policy = stored.policy;
184
214
  existing.createdAt = stored.createdAt;
185
215
  existing.expiresAt = stored.expiresAt;
186
216
  existing.lastUsedAt = stored.lastUsedAt;
@@ -193,6 +223,7 @@ export class ApiTokenManager {
193
223
  doc.name = stored.name;
194
224
  doc.tokenHash = stored.tokenHash;
195
225
  doc.scopes = stored.scopes;
226
+ doc.policy = stored.policy;
196
227
  doc.createdAt = stored.createdAt;
197
228
  doc.expiresAt = stored.expiresAt;
198
229
  doc.lastUsedAt = stored.lastUsedAt;
@@ -256,6 +256,15 @@ export class RouteConfigManager {
256
256
  return this.updateRoute(id, { enabled });
257
257
  }
258
258
 
259
+ public findApiRouteByExternalKey(externalKey: string): IRoute | undefined {
260
+ for (const route of this.routes.values()) {
261
+ if (route.origin === 'api' && route.metadata?.externalKey === externalKey) {
262
+ return route;
263
+ }
264
+ }
265
+ return undefined;
266
+ }
267
+
259
268
  // =========================================================================
260
269
  // Private: seed routes from constructor config
261
270
  // =========================================================================
@@ -443,6 +452,20 @@ export class RouteConfigManager {
443
452
  lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
444
453
  ? metadata.lastResolvedAt
445
454
  : undefined,
455
+ ownerType: metadata.ownerType === 'gatewayClient' || metadata.ownerType === 'workhoster' || metadata.ownerType === 'operator' || metadata.ownerType === 'system'
456
+ ? metadata.ownerType
457
+ : undefined,
458
+ gatewayClientType: metadata.gatewayClientType === 'onebox' || metadata.gatewayClientType === 'cloudly' || metadata.gatewayClientType === 'custom'
459
+ ? metadata.gatewayClientType
460
+ : metadata.workHosterType,
461
+ gatewayClientId: normalizeString(metadata.gatewayClientId || metadata.workHosterId),
462
+ gatewayClientAppId: normalizeString(metadata.gatewayClientAppId || metadata.workAppId),
463
+ workHosterType: metadata.workHosterType === 'onebox' || metadata.workHosterType === 'cloudly' || metadata.workHosterType === 'custom'
464
+ ? metadata.workHosterType
465
+ : metadata.gatewayClientType,
466
+ workHosterId: normalizeString(metadata.workHosterId || metadata.gatewayClientId),
467
+ workAppId: normalizeString(metadata.workAppId || metadata.gatewayClientAppId),
468
+ externalKey: normalizeString(metadata.externalKey),
446
469
  };
447
470
 
448
471
  if (!normalized.sourceProfileRef) {
@@ -454,6 +477,20 @@ export class RouteConfigManager {
454
477
  if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
455
478
  normalized.lastResolvedAt = undefined;
456
479
  }
480
+ if (normalized.ownerType !== 'gatewayClient' && normalized.ownerType !== 'workhoster') {
481
+ normalized.gatewayClientType = undefined;
482
+ normalized.gatewayClientId = undefined;
483
+ normalized.gatewayClientAppId = undefined;
484
+ normalized.workHosterType = undefined;
485
+ normalized.workHosterId = undefined;
486
+ normalized.workAppId = undefined;
487
+ normalized.externalKey = undefined;
488
+ } else {
489
+ normalized.ownerType = 'gatewayClient';
490
+ normalized.workHosterType = normalized.gatewayClientType;
491
+ normalized.workHosterId = normalized.gatewayClientId;
492
+ normalized.workAppId = normalized.gatewayClientAppId;
493
+ }
457
494
 
458
495
  if (Object.values(normalized).every((value) => value === undefined)) {
459
496
  return undefined;
@@ -1,6 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { DcRouterDb } from '../classes.dcrouter-db.js';
3
- import type { TApiTokenScope } from '../../../ts_interfaces/data/route-management.js';
3
+ import type { IApiTokenPolicy, TApiTokenScope } from '../../../ts_interfaces/data/route-management.js';
4
4
 
5
5
  const getDb = () => DcRouterDb.getInstance().getDb();
6
6
 
@@ -19,6 +19,9 @@ export class ApiTokenDoc extends plugins.smartdata.SmartDataDbDoc<ApiTokenDoc, A
19
19
  @plugins.smartdata.svDb()
20
20
  public scopes!: TApiTokenScope[];
21
21
 
22
+ @plugins.smartdata.svDb()
23
+ public policy?: IApiTokenPolicy;
24
+
22
25
  @plugins.smartdata.svDb()
23
26
  public createdAt!: number;
24
27
 
@@ -25,6 +25,9 @@ export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntell
25
25
  @plugins.smartdata.svDb()
26
26
  public networkRange: string | null = null;
27
27
 
28
+ @plugins.smartdata.svDb()
29
+ public networkCidrs: string[] | null = null;
30
+
28
31
  @plugins.smartdata.svDb()
29
32
  public abuseContact: string | null = null;
30
33
 
@@ -57,6 +57,31 @@ export class EmailDomainManager {
57
57
  return doc ? this.docToInterface(doc) : null;
58
58
  }
59
59
 
60
+ public async getByDomain(domainName: string): Promise<IEmailDomain | null> {
61
+ const doc = await EmailDomainDoc.findByDomain(domainName);
62
+ return doc ? this.docToInterface(doc) : null;
63
+ }
64
+
65
+ public async ensureEmailDomainForDomainName(domainName: string): Promise<IEmailDomain | null> {
66
+ const normalizedDomain = domainName.trim().toLowerCase();
67
+ const existing = await this.getByDomain(normalizedDomain);
68
+ if (existing) return existing;
69
+ if (this.isDomainAlreadyConfigured(normalizedDomain)) return null;
70
+
71
+ const linkedDomain = await this.findLinkedDnsDomain(normalizedDomain);
72
+ if (!linkedDomain) {
73
+ throw new Error(`DNS domain not found for email domain: ${normalizedDomain}`);
74
+ }
75
+
76
+ const subdomain = normalizedDomain === linkedDomain.name
77
+ ? undefined
78
+ : normalizedDomain.slice(0, -(linkedDomain.name.length + 1));
79
+ return await this.createEmailDomain({
80
+ linkedDomainId: linkedDomain.id,
81
+ subdomain,
82
+ });
83
+ }
84
+
60
85
  public async createEmailDomain(opts: {
61
86
  linkedDomainId: string;
62
87
  subdomain?: string;
@@ -351,6 +376,13 @@ export class EmailDomainManager {
351
376
  return configuredDomains.includes(domainName.toLowerCase());
352
377
  }
353
378
 
379
+ private async findLinkedDnsDomain(domainName: string): Promise<DomainDoc | null> {
380
+ const domains = await DomainDoc.findAll();
381
+ return domains
382
+ .filter((domainDoc) => domainName === domainDoc.name || domainName.endsWith(`.${domainDoc.name}`))
383
+ .sort((a, b) => b.name.length - a.name.length)[0] || null;
384
+ }
385
+
354
386
  private async buildManagedDomainConfigs(): Promise<IEmailDomainConfig[]> {
355
387
  const docs = await EmailDomainDoc.findAll();
356
388
  const managedConfigs: IEmailDomainConfig[] = [];
@@ -378,7 +410,7 @@ export class EmailDomainManager {
378
410
  return managedConfigs;
379
411
  }
380
412
 
381
- private async syncManagedDomainsToRuntime(): Promise<void> {
413
+ public async syncManagedDomainsToRuntime(): Promise<void> {
382
414
  if (!this.dcRouter.options?.emailConfig) {
383
415
  return;
384
416
  }
@@ -0,0 +1,343 @@
1
+ import type {
2
+ IEmailRoute,
3
+ IUnifiedEmailServerOptions,
4
+ } from '@push.rocks/smartmta';
5
+ import * as plugins from '../plugins.js';
6
+ import type * as interfaces from '../../ts_interfaces/index.js';
7
+
8
+ type TSyncRequest = interfaces.requests.IReq_SyncWorkAppMailIdentity['request'];
9
+
10
+ interface IStoredWorkAppMailIdentity extends interfaces.data.IWorkAppMailIdentity {
11
+ smtpPassword: string;
12
+ }
13
+
14
+ interface IStoredWorkAppMailState {
15
+ version: 1;
16
+ identities: IStoredWorkAppMailIdentity[];
17
+ }
18
+
19
+ export class WorkAppMailManager {
20
+ private readonly storageKey = '/workhosters/mail-identities.json';
21
+
22
+ constructor(private dcRouterRef: any) {}
23
+
24
+ public async listMailIdentities(
25
+ ownership?: Partial<interfaces.data.IWorkAppMailOwnership>,
26
+ ): Promise<interfaces.data.IWorkAppMailIdentity[]> {
27
+ const identities = await this.readStoredIdentities();
28
+ return identities
29
+ .filter((identity) => this.matchesOwnership(identity.ownership, ownership))
30
+ .map((identity) => this.toPublicIdentity(identity));
31
+ }
32
+
33
+ public async syncMailIdentity(
34
+ request: TSyncRequest,
35
+ createdBy: string,
36
+ ): Promise<interfaces.data.IWorkAppMailIdentitySyncResult> {
37
+ if (!this.dcRouterRef.options.emailConfig) {
38
+ return { success: false, message: 'Email server is not configured' };
39
+ }
40
+
41
+ const ownership = this.normalizeOwnership(request.ownership);
42
+ const domain = this.normalizeDomain(request.domain);
43
+ const localPart = this.normalizeLocalPart(request.localPart);
44
+ const address = `${localPart}@${domain}`;
45
+ const externalKey = this.buildExternalKey(ownership, address);
46
+ const identities = await this.readStoredIdentities();
47
+ const existingIndex = identities.findIndex((identity) => identity.externalKey === externalKey);
48
+
49
+ if (request.delete) {
50
+ if (existingIndex < 0) {
51
+ return { success: true, action: 'unchanged' };
52
+ }
53
+ const [deletedIdentity] = identities.splice(existingIndex, 1);
54
+ await this.writeStoredIdentities(identities);
55
+ await this.applyStoredIdentitiesToRuntime(identities);
56
+ return {
57
+ success: true,
58
+ action: 'deleted',
59
+ identity: this.toPublicIdentity(deletedIdentity),
60
+ };
61
+ }
62
+
63
+ await this.ensureEmailDomainConfigured(domain);
64
+
65
+ const existingIdentity = existingIndex >= 0 ? identities[existingIndex] : undefined;
66
+ const now = Date.now();
67
+ const smtpPassword = existingIdentity && !request.resetSmtpPassword
68
+ ? existingIdentity.smtpPassword
69
+ : this.generateSmtpPassword();
70
+ const identity: IStoredWorkAppMailIdentity = {
71
+ id: existingIdentity?.id || plugins.smartunique.shortId(),
72
+ externalKey,
73
+ ownership,
74
+ address,
75
+ localPart,
76
+ domain,
77
+ enabled: request.enabled ?? existingIdentity?.enabled ?? true,
78
+ displayName: request.displayName ?? existingIdentity?.displayName,
79
+ inbound: this.normalizeInboundRoute(request.inbound ?? existingIdentity?.inbound),
80
+ smtp: {
81
+ enabled: request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true,
82
+ username: existingIdentity?.smtp.username || this.buildSmtpUsername(externalKey),
83
+ },
84
+ createdAt: existingIdentity?.createdAt || now,
85
+ updatedAt: now,
86
+ createdBy: existingIdentity?.createdBy || createdBy,
87
+ smtpPassword,
88
+ };
89
+
90
+ if (existingIndex >= 0) {
91
+ identities[existingIndex] = identity;
92
+ } else {
93
+ identities.push(identity);
94
+ }
95
+
96
+ await this.writeStoredIdentities(identities);
97
+ await this.applyStoredIdentitiesToRuntime(identities);
98
+
99
+ const response: interfaces.data.IWorkAppMailIdentitySyncResult = {
100
+ success: true,
101
+ action: existingIndex >= 0 ? 'updated' : 'created',
102
+ identity: this.toPublicIdentity(identity),
103
+ };
104
+
105
+ if (existingIndex < 0 || request.resetSmtpPassword) {
106
+ response.smtpCredentials = this.buildSmtpCredentials(identity);
107
+ }
108
+
109
+ return response;
110
+ }
111
+
112
+ public async applyStoredIdentitiesToEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
113
+ emailConfig: TConfig,
114
+ ): Promise<TConfig> {
115
+ const identities = await this.readStoredIdentities();
116
+ return this.mergeIdentitiesIntoEmailConfig(emailConfig, identities);
117
+ }
118
+
119
+ public async applyStoredIdentitiesToRuntime(
120
+ identities = undefined as IStoredWorkAppMailIdentity[] | undefined,
121
+ ): Promise<void> {
122
+ const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
123
+ if (!emailConfig) return;
124
+
125
+ const nextConfig = this.mergeIdentitiesIntoEmailConfig(
126
+ emailConfig,
127
+ identities || await this.readStoredIdentities(),
128
+ );
129
+
130
+ this.dcRouterRef.options.emailConfig = nextConfig;
131
+ if (this.dcRouterRef.emailServer) {
132
+ this.dcRouterRef.emailServer.updateOptions({ auth: nextConfig.auth });
133
+ await this.dcRouterRef.updateEmailRoutes(nextConfig.routes);
134
+ }
135
+ }
136
+
137
+ private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
138
+ const storedData = await this.dcRouterRef.storageManager.get(this.storageKey);
139
+ if (!storedData) return [];
140
+ const parsed = JSON.parse(storedData) as IStoredWorkAppMailState | IStoredWorkAppMailIdentity[];
141
+ return Array.isArray(parsed) ? parsed : parsed.identities || [];
142
+ }
143
+
144
+ private async writeStoredIdentities(identities: IStoredWorkAppMailIdentity[]): Promise<void> {
145
+ const state: IStoredWorkAppMailState = {
146
+ version: 1,
147
+ identities,
148
+ };
149
+ await this.dcRouterRef.storageManager.set(this.storageKey, JSON.stringify(state, null, 2));
150
+ }
151
+
152
+ private mergeIdentitiesIntoEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
153
+ emailConfig: TConfig,
154
+ identities: IStoredWorkAppMailIdentity[],
155
+ ): TConfig {
156
+ const generatedRoutes = identities
157
+ .filter((identity) => identity.enabled && identity.inbound?.enabled)
158
+ .map((identity) => this.buildInboundRoute(identity));
159
+ const configuredRoutes = (emailConfig.routes || [])
160
+ .filter((route) => !this.isManagedMailRouteName(route.name));
161
+ const generatedUsers = identities
162
+ .filter((identity) => identity.enabled && identity.smtp.enabled)
163
+ .map((identity) => ({
164
+ username: identity.smtp.username,
165
+ password: identity.smtpPassword,
166
+ }));
167
+ const configuredUsers = (emailConfig.auth?.users || [])
168
+ .filter((user) => !this.isManagedSmtpUsername(user.username));
169
+
170
+ return {
171
+ ...emailConfig,
172
+ routes: [...configuredRoutes, ...generatedRoutes],
173
+ auth: {
174
+ ...(emailConfig.auth || {}),
175
+ users: [...configuredUsers, ...generatedUsers],
176
+ },
177
+ };
178
+ }
179
+
180
+ private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
181
+ const inbound = identity.inbound!;
182
+ return {
183
+ name: this.buildRouteName(identity.externalKey),
184
+ priority: 1000,
185
+ match: {
186
+ recipients: identity.address,
187
+ },
188
+ action: {
189
+ type: 'forward',
190
+ forward: {
191
+ host: inbound.targetHost,
192
+ port: inbound.targetPort,
193
+ preserveHeaders: inbound.preserveHeaders ?? true,
194
+ addHeaders: {
195
+ 'X-Dcrouter-WorkHoster-Type': identity.ownership.workHosterType,
196
+ 'X-Dcrouter-WorkHoster-Id': identity.ownership.workHosterId,
197
+ 'X-Dcrouter-WorkApp-Id': identity.ownership.workAppId,
198
+ ...(inbound.addHeaders || {}),
199
+ },
200
+ },
201
+ },
202
+ };
203
+ }
204
+
205
+ private async ensureEmailDomainConfigured(domain: string): Promise<void> {
206
+ const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
207
+ if (emailConfig?.domains?.some((domainConfig) => domainConfig.domain.toLowerCase() === domain)) {
208
+ return;
209
+ }
210
+
211
+ const emailDomainManager = this.dcRouterRef.emailDomainManager;
212
+ if (!emailDomainManager) {
213
+ throw new Error(`Email domain is not configured: ${domain}`);
214
+ }
215
+
216
+ if (await emailDomainManager.getByDomain(domain)) {
217
+ await emailDomainManager.syncManagedDomainsToRuntime();
218
+ return;
219
+ }
220
+
221
+ await emailDomainManager.ensureEmailDomainForDomainName(domain);
222
+ }
223
+
224
+ private normalizeOwnership(
225
+ ownership: interfaces.data.IWorkAppMailOwnership,
226
+ ): interfaces.data.IWorkAppMailOwnership {
227
+ const workHosterType = ownership.workHosterType;
228
+ const workHosterId = ownership.workHosterId?.trim();
229
+ const workAppId = ownership.workAppId?.trim();
230
+ if (!['onebox', 'cloudly', 'custom'].includes(workHosterType)) {
231
+ throw new Error(`Invalid WorkHoster type: ${workHosterType}`);
232
+ }
233
+ if (!workHosterId) throw new Error('workHosterId is required');
234
+ if (!workAppId) throw new Error('workAppId is required');
235
+ return { workHosterType, workHosterId, workAppId };
236
+ }
237
+
238
+ private normalizeDomain(domain: string): string {
239
+ const normalized = domain?.trim().toLowerCase();
240
+ if (!normalized || normalized.includes('@') || !normalized.includes('.')) {
241
+ throw new Error(`Invalid email domain: ${domain}`);
242
+ }
243
+ return normalized;
244
+ }
245
+
246
+ private normalizeLocalPart(localPart: string): string {
247
+ const normalized = localPart?.trim().toLowerCase();
248
+ if (!normalized || normalized.includes('@') || /\s/.test(normalized)) {
249
+ throw new Error(`Invalid email local part: ${localPart}`);
250
+ }
251
+ return normalized;
252
+ }
253
+
254
+ private normalizeInboundRoute(
255
+ inbound?: interfaces.data.IWorkAppMailInboundRoute,
256
+ ): interfaces.data.IWorkAppMailInboundRoute | undefined {
257
+ if (!inbound) return undefined;
258
+ if (!inbound.enabled) {
259
+ return { ...inbound, enabled: false };
260
+ }
261
+ const targetHost = inbound.targetHost?.trim();
262
+ const targetPort = Number(inbound.targetPort);
263
+ if (!targetHost) throw new Error('inbound.targetHost is required when inbound routing is enabled');
264
+ if (!Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535) {
265
+ throw new Error(`Invalid inbound.targetPort: ${inbound.targetPort}`);
266
+ }
267
+ return {
268
+ ...inbound,
269
+ targetHost,
270
+ targetPort,
271
+ };
272
+ }
273
+
274
+ private matchesOwnership(
275
+ ownership: interfaces.data.IWorkAppMailOwnership,
276
+ filter?: Partial<interfaces.data.IWorkAppMailOwnership>,
277
+ ): boolean {
278
+ if (!filter) return true;
279
+ if (filter.workHosterType && filter.workHosterType !== ownership.workHosterType) return false;
280
+ if (filter.workHosterId && filter.workHosterId !== ownership.workHosterId) return false;
281
+ if (filter.workAppId && filter.workAppId !== ownership.workAppId) return false;
282
+ return true;
283
+ }
284
+
285
+ private buildExternalKey(
286
+ ownership: interfaces.data.IWorkAppMailOwnership,
287
+ address: string,
288
+ ): string {
289
+ return [
290
+ ownership.workHosterType,
291
+ ownership.workHosterId,
292
+ ownership.workAppId,
293
+ address,
294
+ ].join(':');
295
+ }
296
+
297
+ private buildSmtpUsername(externalKey: string): string {
298
+ return `workapp-${this.hashExternalKey(externalKey).slice(0, 24)}`;
299
+ }
300
+
301
+ private buildRouteName(externalKey: string): string {
302
+ return `workapp-mail-${this.hashExternalKey(externalKey).slice(0, 32)}`;
303
+ }
304
+
305
+ private hashExternalKey(externalKey: string): string {
306
+ return plugins.crypto.createHash('sha256').update(externalKey).digest('hex');
307
+ }
308
+
309
+ private generateSmtpPassword(): string {
310
+ return plugins.crypto.randomBytes(24).toString('base64url');
311
+ }
312
+
313
+ private isManagedMailRouteName(routeName: string): boolean {
314
+ return routeName.startsWith('workapp-mail-');
315
+ }
316
+
317
+ private isManagedSmtpUsername(username: string): boolean {
318
+ return username.startsWith('workapp-');
319
+ }
320
+
321
+ private buildSmtpCredentials(
322
+ identity: IStoredWorkAppMailIdentity,
323
+ ): interfaces.data.IWorkAppMailCredentials {
324
+ return {
325
+ username: identity.smtp.username,
326
+ password: identity.smtpPassword,
327
+ host: this.dcRouterRef.options.emailConfig?.outbound?.hostname
328
+ || this.dcRouterRef.options.emailConfig?.hostname,
329
+ ports: {
330
+ smtp: this.dcRouterRef.options.emailConfig?.ports?.includes(25) ? 25 : undefined,
331
+ submission: this.dcRouterRef.options.emailConfig?.ports?.includes(587) ? 587 : undefined,
332
+ smtps: this.dcRouterRef.options.emailConfig?.ports?.includes(465) ? 465 : undefined,
333
+ },
334
+ };
335
+ }
336
+
337
+ private toPublicIdentity(
338
+ identity: IStoredWorkAppMailIdentity,
339
+ ): interfaces.data.IWorkAppMailIdentity {
340
+ const { smtpPassword, ...publicIdentity } = identity;
341
+ return publicIdentity;
342
+ }
343
+ }
package/ts/email/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './classes.email-domain.manager.js';
2
2
  export * from './classes.smartmta-storage-manager.js';
3
+ export * from './classes.workapp-mail-manager.js';
3
4
  export * from './email-dns-records.js';
@@ -38,6 +38,7 @@ export class OpsServer {
38
38
  private dnsRecordHandler!: handlers.DnsRecordHandler;
39
39
  private acmeConfigHandler!: handlers.AcmeConfigHandler;
40
40
  private emailDomainHandler!: handlers.EmailDomainHandler;
41
+ private workHosterHandler!: handlers.WorkHosterHandler;
41
42
 
42
43
  constructor(dcRouterRefArg: DcRouter) {
43
44
  this.dcRouterRef = dcRouterRefArg;
@@ -106,6 +107,7 @@ export class OpsServer {
106
107
  this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
107
108
  this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
108
109
  this.emailDomainHandler = new handlers.EmailDomainHandler(this);
110
+ this.workHosterHandler = new handlers.WorkHosterHandler(this);
109
111
 
110
112
  console.log('✅ OpsServer TypedRequest handlers initialized');
111
113
  }
@@ -119,4 +121,4 @@ export class OpsServer {
119
121
  await this.server.stop();
120
122
  }
121
123
  }
122
- }
124
+ }
@@ -43,14 +43,21 @@ export class AdminHandler {
43
43
  }
44
44
 
45
45
  private initializeDefaultUsers(): void {
46
- // Add default admin user
46
+ const username = process.env.DCROUTER_ADMIN_USERNAME || 'admin';
47
+ const configuredPassword = process.env.DCROUTER_ADMIN_PASSWORD;
48
+ const password = configuredPassword || plugins.crypto.randomBytes(24).toString('base64url');
49
+
47
50
  const adminId = plugins.uuid.v4();
48
51
  this.users.set(adminId, {
49
52
  id: adminId,
50
- username: 'admin',
51
- password: 'admin',
53
+ username,
54
+ password,
52
55
  role: 'admin',
53
56
  });
57
+
58
+ if (!configuredPassword) {
59
+ console.warn(`DCRouter generated one-time admin password for ${username}: ${password}`);
60
+ }
54
61
  }
55
62
 
56
63
  /**
@@ -249,4 +256,4 @@ export class AdminHandler {
249
256
  name: 'adminIdentityGuard',
250
257
  }
251
258
  );
252
- }
259
+ }
@@ -26,6 +26,7 @@ export class ApiTokenHandler {
26
26
  dataArg.scopes,
27
27
  dataArg.expiresInDays ?? null,
28
28
  dataArg.identity.userId,
29
+ dataArg.policy,
29
30
  );
30
31
  return { success: true, tokenId: result.id, tokenValue: result.rawToken };
31
32
  },