feat(email-domains): support creating email domains on optional subdomains

This commit is contained in:
2026-04-12 23:46:31 +00:00
parent 2cdc392a40
commit 59b52d08fa
12 changed files with 65 additions and 16 deletions

View File

@@ -1,5 +1,12 @@
# Changelog
## 2026-04-12 - 13.12.0 - feat(email-domains)
support creating email domains on optional subdomains
- Add optional subdomain support to email domain creation, persistence, and API interfaces.
- Update the ops UI to collect and submit a subdomain prefix when creating an email domain.
- Bump @design.estate/dees-catalog from ^3.78.0 to ^3.78.2.
## 2026-04-12 - 13.11.0 - feat(email-domains)
add email domain management with DNS provisioning, validation, and ops dashboard support

View File

@@ -35,7 +35,7 @@
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.78.0",
"@design.estate/dees-catalog": "^3.78.2",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.1.0",

43
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0
version: 7.1.0
'@design.estate/dees-catalog':
specifier: ^3.78.0
version: 3.78.0(@tiptap/pm@2.27.2)
specifier: ^3.78.2
version: 3.78.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -353,8 +353,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.78.0':
resolution: {integrity: sha512-doc9eYGsFV47Ui7k5FuLXpt3ytC/Q+g+yX+qGU/V4fZpc5KUXpL04/FRzO0AU1wF9Xl9GMmL39CcE2vKj88QAQ==}
'@design.estate/dees-catalog@3.78.2':
resolution: {integrity: sha512-9MKKCvx+vxoIp6UpqVQklreokdg7ZSSODz4FlKyNFqjfZiDDme6pjwxWoMSA+Tn4bkboYyCBosUrVfc0nxa1HA==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -368,8 +368,8 @@ packages:
'@design.estate/dees-wcctools@3.8.0':
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
'@design.estate/dees-wcctools@3.8.4':
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==}
'@design.estate/dees-wcctools@3.9.0':
resolution: {integrity: sha512-0vZBaGBEGIbl8hx+8BezIIea3U5T7iSHHF9VqlJZGf+nOFIW4zBAxcCljH8YzZ1Yayp6BEjxp/pQXjHN2YB3Jg==}
'@emnapi/core@1.9.2':
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
@@ -1997,6 +1997,12 @@ packages:
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
'@types/dom-mediacapture-transform@0.1.11':
resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==}
'@types/dom-webcodecs@0.1.13':
resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==}
'@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
@@ -3166,6 +3172,9 @@ packages:
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
mediabunny@1.40.1:
resolution: {integrity: sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
@@ -4318,7 +4327,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
'@cloudflare/workers-types': 4.20260405.1
'@design.estate/dees-catalog': 3.78.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.78.2(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.0
'@push.rocks/smartdelay': 3.0.5
@@ -4847,11 +4856,11 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.78.0(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.78.2(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.4
'@design.estate/dees-wcctools': 3.9.0
'@fortawesome/fontawesome-svg-core': 7.2.0
'@fortawesome/free-brands-svg-icons': 7.2.0
'@fortawesome/free-regular-svg-icons': 7.2.0
@@ -4940,12 +4949,13 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-wcctools@3.8.4':
'@design.estate/dees-wcctools@3.9.0':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
mediabunny: 1.40.1
transitivePeerDependencies:
- '@nuxt/kit'
- react
@@ -6915,7 +6925,7 @@ snapshots:
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.78.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.78.2(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.0
@@ -7464,6 +7474,12 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/dom-mediacapture-transform@0.1.11':
dependencies:
'@types/dom-webcodecs': 0.1.13
'@types/dom-webcodecs@0.1.13': {}
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
@@ -8819,6 +8835,11 @@ snapshots:
mdurl@2.0.0: {}
mediabunny@1.40.1:
dependencies:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
memory-pager@1.5.0: {}
micromark-core-commonmark@2.0.3:

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.11.0',
version: '13.12.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -20,6 +20,9 @@ export class EmailDomainDoc extends plugins.smartdata.SmartDataDbDoc<EmailDomain
@plugins.smartdata.svDb()
public linkedDomainId: string = '';
@plugins.smartdata.svDb()
public subdomain?: string;
@plugins.smartdata.svDb()
public dkim!: IEmailDomainDkim;

View File

@@ -48,6 +48,7 @@ export class EmailDomainManager {
public async createEmailDomain(opts: {
linkedDomainId: string;
subdomain?: string;
dkimSelector?: string;
dkimKeySize?: number;
rotateKeys?: boolean;
@@ -58,7 +59,9 @@ export class EmailDomainManager {
if (!domainDoc) {
throw new Error(`DNS domain not found: ${opts.linkedDomainId}`);
}
const domainName = domainDoc.name;
const baseDomain = domainDoc.name;
const subdomain = opts.subdomain?.trim() || undefined;
const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;
// Check for duplicates
const existing = await EmailDomainDoc.findByDomain(domainName);
@@ -90,6 +93,7 @@ export class EmailDomainManager {
doc.id = plugins.smartunique.shortId();
doc.domain = domainName.toLowerCase();
doc.linkedDomainId = opts.linkedDomainId;
doc.subdomain = subdomain;
doc.dkim = {
selector,
keySize,
@@ -306,6 +310,7 @@ export class EmailDomainManager {
id: doc.id,
domain: doc.domain,
linkedDomainId: doc.linkedDomainId,
subdomain: doc.subdomain,
dkim: doc.dkim,
rateLimits: doc.rateLimits,
dnsStatus: doc.dnsStatus,

View File

@@ -85,6 +85,7 @@ export class EmailDomainHandler {
try {
const domain = await this.manager.createEmailDomain({
linkedDomainId: dataArg.linkedDomainId,
subdomain: dataArg.subdomain,
dkimSelector: dataArg.dkimSelector,
dkimKeySize: dataArg.dkimKeySize,
rotateKeys: dataArg.rotateKeys,

View File

@@ -12,10 +12,12 @@ export type TDnsRecordStatus = 'valid' | 'missing' | 'invalid' | 'unchecked';
*/
export interface IEmailDomain {
id: string;
/** Fully qualified domain name (e.g. 'example.com'). */
/** Fully qualified email domain name (e.g. 'example.com' or 'mail.example.com'). */
domain: string;
/** ID of the linked dcrouter DNS domain — determines how DNS records are managed. */
linkedDomainId: string;
/** Optional subdomain prefix (e.g. 'mail' for mail.example.com). Empty/undefined = bare domain. */
subdomain?: string;
/** DKIM configuration and key state. */
dkim: IEmailDomainDkim;
/** Optional per-domain rate limits. */

View File

@@ -55,6 +55,8 @@ export interface IReq_CreateEmailDomain extends plugins.typedrequestInterfaces.i
apiToken?: string;
/** ID of the existing dcrouter DNS domain to link to. */
linkedDomainId: string;
/** Optional subdomain prefix (e.g. 'mail' for mail.example.com). Leave empty for bare domain. */
subdomain?: string;
/** DKIM selector (default: 'default'). */
dkimSelector?: string;
/** RSA key size (default: 2048). */

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.11.0',
version: '13.12.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -2422,6 +2422,7 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
export const createEmailDomainAction = emailDomainsStatePart.createAction<{
linkedDomainId: string;
subdomain?: string;
dkimSelector?: string;
dkimKeySize?: number;
rotateKeys?: boolean;

View File

@@ -276,6 +276,11 @@ export class OpsViewEmailDomains extends DeesElement {
.options=${domainOptions}
.required=${true}
></dees-input-dropdown>
<dees-input-text
.key=${'subdomain'}
.label=${'Subdomain'}
.description=${'Leave empty for bare domain, e.g. "mail" for mail.example.com'}
></dees-input-text>
<dees-input-text
.key=${'dkimSelector'}
.label=${'DKIM Selector'}
@@ -316,10 +321,12 @@ export class OpsViewEmailDomains extends DeesElement {
? parseInt(data.dkimKeySize.key, 10)
: parseInt(data.dkimKeySize || '2048', 10);
const subdomain = data.subdomain?.trim() || undefined;
await appstate.emailDomainsStatePart.dispatchAction(
appstate.createEmailDomainAction,
{
linkedDomainId,
subdomain,
dkimSelector: data.dkimSelector || 'default',
dkimKeySize: keySize,
rotateKeys: Boolean(data.rotateKeys),