@serve.zone/dcrouter 13.11.0 → 13.13.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 (36) hide show
  1. package/dist_serve/bundle.js +333 -305
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +6 -5
  4. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +1 -0
  5. package/dist_ts/db/documents/classes.email-domain.doc.js +8 -2
  6. package/dist_ts/dns/manager.dns.d.ts +46 -8
  7. package/dist_ts/dns/manager.dns.js +189 -36
  8. package/dist_ts/email/classes.email-domain.manager.d.ts +1 -0
  9. package/dist_ts/email/classes.email-domain.manager.js +6 -2
  10. package/dist_ts/opsserver/handlers/config.handler.js +2 -2
  11. package/dist_ts/opsserver/handlers/domain.handler.js +14 -1
  12. package/dist_ts/opsserver/handlers/email-domain.handler.js +2 -1
  13. package/dist_ts_interfaces/data/email-domain.d.ts +3 -1
  14. package/dist_ts_interfaces/requests/domains.d.ts +24 -0
  15. package/dist_ts_interfaces/requests/email-domains.d.ts +2 -0
  16. package/dist_ts_web/00_commitinfo_data.js +1 -1
  17. package/dist_ts_web/appstate.d.ts +7 -0
  18. package/dist_ts_web/appstate.js +18 -1
  19. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +1 -0
  20. package/dist_ts_web/elements/domains/ops-view-domains.js +97 -1
  21. package/dist_ts_web/elements/email/ops-view-email-domains.js +8 -1
  22. package/package.json +2 -2
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/classes.dcrouter.ts +5 -4
  25. package/ts/db/documents/classes.email-domain.doc.ts +3 -0
  26. package/ts/dns/manager.dns.ts +219 -35
  27. package/ts/email/classes.email-domain.manager.ts +6 -1
  28. package/ts/opsserver/handlers/config.handler.ts +1 -1
  29. package/ts/opsserver/handlers/domain.handler.ts +18 -0
  30. package/ts/opsserver/handlers/email-domain.handler.ts +1 -0
  31. package/ts_web/00_commitinfo_data.ts +1 -1
  32. package/ts_web/appstate.ts +27 -0
  33. package/ts_web/elements/domains/ops-view-domains.ts +99 -0
  34. package/ts_web/elements/email/ops-view-email-domains.ts +7 -0
  35. package/dist_ts_oci_container/plugins.d.ts +0 -3
  36. package/dist_ts_oci_container/plugins.js +0 -4
@@ -48,6 +48,7 @@ export class EmailDomainManager {
48
48
 
49
49
  public async createEmailDomain(opts: {
50
50
  linkedDomainId: string;
51
+ subdomain?: string;
51
52
  dkimSelector?: string;
52
53
  dkimKeySize?: number;
53
54
  rotateKeys?: boolean;
@@ -58,7 +59,9 @@ export class EmailDomainManager {
58
59
  if (!domainDoc) {
59
60
  throw new Error(`DNS domain not found: ${opts.linkedDomainId}`);
60
61
  }
61
- const domainName = domainDoc.name;
62
+ const baseDomain = domainDoc.name;
63
+ const subdomain = opts.subdomain?.trim() || undefined;
64
+ const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;
62
65
 
63
66
  // Check for duplicates
64
67
  const existing = await EmailDomainDoc.findByDomain(domainName);
@@ -90,6 +93,7 @@ export class EmailDomainManager {
90
93
  doc.id = plugins.smartunique.shortId();
91
94
  doc.domain = domainName.toLowerCase();
92
95
  doc.linkedDomainId = opts.linkedDomainId;
96
+ doc.subdomain = subdomain;
93
97
  doc.dkim = {
94
98
  selector,
95
99
  keySize,
@@ -306,6 +310,7 @@ export class EmailDomainManager {
306
310
  id: doc.id,
307
311
  domain: doc.domain,
308
312
  linkedDomainId: doc.linkedDomainId,
313
+ subdomain: doc.subdomain,
309
314
  dkim: doc.dkim,
310
315
  rateLimits: doc.rateLimits,
311
316
  dnsStatus: doc.dnsStatus,
@@ -127,7 +127,7 @@ export class ConfigHandler {
127
127
  // (replaces the legacy `dnsChallenge.cloudflareApiKey` constructor field).
128
128
  let dnsChallengeEnabled = false;
129
129
  try {
130
- dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAcmeCapableProvider()) ?? false;
130
+ dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAnyManagedDomain()) ?? false;
131
131
  } catch {
132
132
  dnsChallengeEnabled = false;
133
133
  }
@@ -157,5 +157,23 @@ export class DomainHandler {
157
157
  },
158
158
  ),
159
159
  );
160
+
161
+ // Migrate domain between dcrouter-hosted and provider-managed
162
+ this.typedrouter.addTypedHandler(
163
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_MigrateDomain>(
164
+ 'migrateDomain',
165
+ async (dataArg) => {
166
+ await this.requireAuth(dataArg, 'domains:write');
167
+ const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
168
+ if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
169
+ return await dnsManager.migrateDomain({
170
+ id: dataArg.id,
171
+ targetSource: dataArg.targetSource,
172
+ targetProviderId: dataArg.targetProviderId,
173
+ deleteExistingProviderRecords: dataArg.deleteExistingProviderRecords,
174
+ });
175
+ },
176
+ ),
177
+ );
160
178
  }
161
179
  }
@@ -85,6 +85,7 @@ export class EmailDomainHandler {
85
85
  try {
86
86
  const domain = await this.manager.createEmailDomain({
87
87
  linkedDomainId: dataArg.linkedDomainId,
88
+ subdomain: dataArg.subdomain,
88
89
  dkimSelector: dataArg.dkimSelector,
89
90
  dkimKeySize: dataArg.dkimKeySize,
90
91
  rotateKeys: dataArg.rotateKeys,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.11.0',
6
+ version: '13.13.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -1887,6 +1887,32 @@ export const syncDomainAction = domainsStatePart.createAction<{ id: string }>(
1887
1887
  },
1888
1888
  );
1889
1889
 
1890
+ export const migrateDomainAction = domainsStatePart.createAction<{
1891
+ id: string;
1892
+ targetSource: interfaces.data.TDomainSource;
1893
+ targetProviderId?: string;
1894
+ deleteExistingProviderRecords?: boolean;
1895
+ }>(
1896
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1897
+ const context = getActionContext();
1898
+ try {
1899
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1900
+ interfaces.requests.IReq_MigrateDomain
1901
+ >('/typedrequest', 'migrateDomain');
1902
+ const response = await request.fire({ identity: context.identity!, ...dataArg });
1903
+ if (!response.success) {
1904
+ return { ...statePartArg.getState()!, error: response.message || 'Migration failed' };
1905
+ }
1906
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1907
+ } catch (error: unknown) {
1908
+ return {
1909
+ ...statePartArg.getState()!,
1910
+ error: error instanceof Error ? error.message : 'Migration failed',
1911
+ };
1912
+ }
1913
+ },
1914
+ );
1915
+
1890
1916
  export const createDnsRecordAction = domainsStatePart.createAction<{
1891
1917
  domainId: string;
1892
1918
  name: string;
@@ -2422,6 +2448,7 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
2422
2448
 
2423
2449
  export const createEmailDomainAction = emailDomainsStatePart.createAction<{
2424
2450
  linkedDomainId: string;
2451
+ subdomain?: string;
2425
2452
  dkimSelector?: string;
2426
2453
  dkimKeySize?: number;
2427
2454
  rotateKeys?: boolean;
@@ -149,6 +149,15 @@ export class OpsViewDomains extends DeesElement {
149
149
  });
150
150
  },
151
151
  },
152
+ {
153
+ name: 'Migrate',
154
+ iconName: 'lucide:arrow-right-left',
155
+ type: ['inRow', 'contextmenu'] as any,
156
+ actionFunc: async (actionData: any) => {
157
+ const domain = actionData.item as interfaces.data.IDomain;
158
+ await this.showMigrateDialog(domain);
159
+ },
160
+ },
152
161
  {
153
162
  name: 'Delete',
154
163
  iconName: 'lucide:trash2',
@@ -308,6 +317,96 @@ export class OpsViewDomains extends DeesElement {
308
317
  });
309
318
  }
310
319
 
320
+ private async showMigrateDialog(domain: interfaces.data.IDomain) {
321
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
322
+ const providers = this.domainsState.providers;
323
+
324
+ // Build target options based on current source
325
+ const targetOptions: { option: string; key: string }[] = [];
326
+ if (domain.source === 'provider') {
327
+ targetOptions.push({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
328
+ }
329
+ // Add all providers (except the current one if already provider-managed)
330
+ for (const p of providers) {
331
+ if (domain.source === 'provider' && domain.providerId === p.id) continue;
332
+ targetOptions.push({ option: `${p.name} (${p.type})`, key: `provider:${p.id}` });
333
+ }
334
+ if (domain.source === 'dcrouter') {
335
+ targetOptions.unshift({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
336
+ }
337
+
338
+ if (targetOptions.length === 0) {
339
+ DeesToast.show({
340
+ message: 'No migration targets available. Add a DNS provider first.',
341
+ type: 'warning',
342
+ duration: 3000,
343
+ });
344
+ return;
345
+ }
346
+
347
+ const currentLabel = domain.source === 'dcrouter'
348
+ ? 'DcRouter (authoritative)'
349
+ : providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
350
+
351
+ DeesModal.createAndShow({
352
+ heading: `Migrate: ${domain.name}`,
353
+ content: html`
354
+ <dees-form>
355
+ <dees-input-text
356
+ .key=${'currentSource'}
357
+ .label=${'Current source'}
358
+ .value=${currentLabel}
359
+ .disabled=${true}
360
+ ></dees-input-text>
361
+ <dees-input-dropdown
362
+ .key=${'target'}
363
+ .label=${'Migrate to'}
364
+ .description=${'Select the target DNS management'}
365
+ .options=${targetOptions}
366
+ .required=${true}
367
+ ></dees-input-dropdown>
368
+ <dees-input-checkbox
369
+ .key=${'deleteExisting'}
370
+ .label=${'Delete existing records at provider first'}
371
+ .description=${'Removes all records at the provider before pushing migrated records'}
372
+ .value=${true}
373
+ ></dees-input-checkbox>
374
+ </dees-form>
375
+ `,
376
+ menuOptions: [
377
+ { name: 'Cancel', action: async (m: any) => m.destroy() },
378
+ {
379
+ name: 'Migrate',
380
+ action: async (m: any) => {
381
+ const form = m.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
382
+ if (!form) return;
383
+ const data = await form.collectFormData();
384
+ const targetKey = typeof data.target === 'object' ? data.target.key : data.target;
385
+ if (!targetKey) return;
386
+
387
+ let targetSource: interfaces.data.TDomainSource;
388
+ let targetProviderId: string | undefined;
389
+ if (targetKey === 'dcrouter') {
390
+ targetSource = 'dcrouter';
391
+ } else {
392
+ targetSource = 'provider';
393
+ targetProviderId = targetKey.replace('provider:', '');
394
+ }
395
+
396
+ await appstate.domainsStatePart.dispatchAction(appstate.migrateDomainAction, {
397
+ id: domain.id,
398
+ targetSource,
399
+ targetProviderId,
400
+ deleteExistingProviderRecords: targetSource === 'provider' ? Boolean(data.deleteExisting) : false,
401
+ });
402
+ DeesToast.show({ message: `Domain ${domain.name} migrated successfully`, type: 'success', duration: 3000 });
403
+ m.destroy();
404
+ },
405
+ },
406
+ ],
407
+ });
408
+ }
409
+
311
410
  private async deleteDomain(domain: interfaces.data.IDomain) {
312
411
  const { DeesModal } = await import('@design.estate/dees-catalog');
313
412
  DeesModal.createAndShow({
@@ -276,6 +276,11 @@ export class OpsViewEmailDomains extends DeesElement {
276
276
  .options=${domainOptions}
277
277
  .required=${true}
278
278
  ></dees-input-dropdown>
279
+ <dees-input-text
280
+ .key=${'subdomain'}
281
+ .label=${'Subdomain'}
282
+ .description=${'Leave empty for bare domain, e.g. "mail" for mail.example.com'}
283
+ ></dees-input-text>
279
284
  <dees-input-text
280
285
  .key=${'dkimSelector'}
281
286
  .label=${'DKIM Selector'}
@@ -316,10 +321,12 @@ export class OpsViewEmailDomains extends DeesElement {
316
321
  ? parseInt(data.dkimKeySize.key, 10)
317
322
  : parseInt(data.dkimKeySize || '2048', 10);
318
323
 
324
+ const subdomain = data.subdomain?.trim() || undefined;
319
325
  await appstate.emailDomainsStatePart.dispatchAction(
320
326
  appstate.createEmailDomainAction,
321
327
  {
322
328
  linkedDomainId,
329
+ subdomain,
323
330
  dkimSelector: data.dkimSelector || 'default',
324
331
  dkimKeySize: keySize,
325
332
  rotateKeys: Boolean(data.rotateKeys),
@@ -1,3 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- export { fs, path, };
@@ -1,4 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- export { fs, path, };
4
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX29jaV9jb250YWluZXIvcGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxNQUFNLElBQUksQ0FBQztBQUN6QixPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUU3QixPQUFPLEVBQ0wsRUFBRSxFQUNGLElBQUksR0FDTCxDQUFDIn0=