Compare commits

...

2 Commits

Author SHA1 Message Date
jkunz ba67e0d208 v14.0.0
Docker (tags) / release (push) Failing after 1s
Release / build-and-release (push) Successful in 6m13s
2026-06-04 16:01:33 +00:00
jkunz e86fe0df7a BREAKING CHANGE(config): remove legacy config seeding and route reprovisioning 2026-06-04 15:51:09 +00:00
20 changed files with 81 additions and 350 deletions
+14 -1
View File
@@ -3,11 +3,24 @@
## Pending
## 2026-06-04 - 14.0.0
### Breaking Changes
- remove legacy config seeding and route-based certificate reprovisioning (config)
- Make ACME configuration DB-backed only and report DB-backed ACME state in the OpsServer config response.
- Stop seeding DNS domains and records from constructor config at runtime.
- Remove the route-name certificate reprovision typed request; domain-based reprovisioning remains available.
- Remove legacy string email-domain normalization from runtime email startup.
### Fixes
- bump @push.rocks/smartproxy to ^27.12.7 (deps)
- Consumes the upstream SmartProxy socket-handler relay fix for server-first SMTP banners.
- Updates the lockfile to resolve @push.rocks/smartproxy 27.12.7.
- use exact SmartData collection names in DNS migrations (migrations)
- Updates DNS source rename migrations to use `DomainDoc` and `DnsRecordDoc` collection names.
- Adds migration coverage for exact SmartData collection names.
## 2026-06-04 - 13.45.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@serve.zone/dcrouter",
"version": "13.45.0",
"version": "14.0.0",
"exports": "./binary/dcrouter.ts",
"compile": {
"include": [
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "13.45.0",
"version": "14.0.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"bin": {
@@ -61,7 +61,7 @@
"@push.rocks/smartnetwork": "^4.7.2",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.4",
"@push.rocks/smartproxy": "^27.12.6",
"@push.rocks/smartproxy": "^27.12.7",
"@push.rocks/smartradius": "^1.3.0",
"@push.rocks/smartrequest": "^5.0.3",
"@push.rocks/smartrx": "^3.0.10",
+5 -5
View File
@@ -84,8 +84,8 @@ importers:
specifier: ^4.2.4
version: 4.2.4
'@push.rocks/smartproxy':
specifier: ^27.12.6
version: 27.12.6
specifier: ^27.12.7
version: 27.12.7
'@push.rocks/smartradius':
specifier: ^1.3.0
version: 1.3.0
@@ -1429,8 +1429,8 @@ packages:
'@push.rocks/smartpromise@4.2.4':
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
'@push.rocks/smartproxy@27.12.6':
resolution: {integrity: sha512-vGUMbv0vwJS2kQ6SqAlhSGsWRoPs4Zk/sELUtNFNpnHWKHlqXeu64FNgiF5mgA9Nz1dfgiFMqErXArzTm8ccOA==}
'@push.rocks/smartproxy@27.12.7':
resolution: {integrity: sha512-5QHQLNUqLn7wrMEP+X361aQSvc4p8RabgV9jPnx4G6DgR8a25Z4kN2PAgtsg75U9QyQbQicE2lyPqIPaSTQ+uQ==}
'@push.rocks/smartpuppeteer@2.0.6':
resolution: {integrity: sha512-G+8cyDERvbXQcb9Sd8lnYdWYz8b3Mv2LfFf1ULmucDqQhcRHvxrWX/dKsvBZrwKPR4Wg+795Dyd+E1iOOh3tHw==}
@@ -6581,7 +6581,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.4': {}
'@push.rocks/smartproxy@27.12.6':
'@push.rocks/smartproxy@27.12.7':
dependencies:
'@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.2
+10 -43
View File
@@ -3,7 +3,6 @@ import { DcRouter } from '../ts/classes.dcrouter.js';
import { ReferenceResolver, RouteConfigManager } from '../ts/config/index.js';
import { DcRouterDb, DnsRecordDoc, DomainDoc, RouteDoc } from '../ts/db/index.js';
import { DnsManager } from '../ts/dns/manager.dns.js';
import { logger } from '../ts/logger.js';
import * as plugins from '../ts/plugins.js';
const createTestDb = async () => {
@@ -411,53 +410,21 @@ tap.test('RouteConfigManager clears remote ingress config when route patch sets
expect(appliedRoutes[appliedRoutes.length - 1][0].remoteIngress).toBeUndefined();
});
tap.test('DnsManager warning keeps dnsNsDomains in scope', async () => {
tap.test('DnsManager start does not seed constructor DNS config into DB', async () => {
await testDbPromise;
await clearTestState();
const originalLog = logger.log.bind(logger);
const warningMessages: string[] = [];
(logger as any).log = (level: 'error' | 'warn' | 'info' | 'success' | 'debug', message: string, context?: Record<string, any>) => {
if (level === 'warn') {
warningMessages.push(message);
}
return originalLog(level, message, context || {});
};
const dnsManager = new DnsManager({
dnsNsDomains: ['ns1.example.com'],
dnsScopes: ['example.com'],
dnsRecords: [{ name: 'www.example.com', type: 'A', value: '127.0.0.1' }],
smartProxyConfig: { routes: [] },
});
try {
const existingDomain = new DomainDoc();
existingDomain.id = 'existing-domain';
existingDomain.name = 'example.com';
existingDomain.source = 'dcrouter';
existingDomain.authoritative = true;
existingDomain.createdAt = Date.now();
existingDomain.updatedAt = Date.now();
existingDomain.createdBy = 'test';
await existingDomain.save();
await dnsManager.start();
const dnsManager = new DnsManager({
dnsNsDomains: ['ns1.example.com'],
dnsScopes: ['example.com'],
dnsRecords: [{ name: 'www.example.com', type: 'A', value: '127.0.0.1' }],
smartProxyConfig: { routes: [] },
});
await dnsManager.start();
expect(
warningMessages.some((message) =>
message.includes('ignoring legacy dnsScopes/dnsRecords constructor config')
&& message.includes('dnsNsDomains is still required for nameserver and DoH bootstrap'),
),
).toEqual(true);
expect(
warningMessages.some((message) =>
message.includes('ignoring legacy dnsScopes/dnsRecords/dnsNsDomains constructor config'),
),
).toEqual(false);
} finally {
(logger as any).log = originalLog;
}
expect(await DomainDoc.findAll()).toHaveLength(0);
expect(await DnsRecordDoc.findAll()).toHaveLength(0);
});
tap.test('cleanup test db', async () => {
+18
View File
@@ -165,6 +165,24 @@ tap.test('migration runner applies schema steps through the current target', asy
expect(sourceProfiles.map((profile) => profile.name)).toContain('PUBLIC');
});
tap.test('migration runner uses exact SmartData collection names for DNS source renames', async () => {
const domains: Array<Record<string, any>> = [{ _id: 'domain-1', source: 'manual' }];
const records: Array<Record<string, any>> = [{ _id: 'record-1', source: 'manual' }];
const runner = await createMigrationRunner(
createFakeDb('13.1.0', {
DomainDoc: domains,
DnsRecordDoc: records,
}),
'13.8.2',
);
const result = await runner.run();
expect(result.stepsApplied).toHaveLength(2);
expect(domains[0].source).toEqual('dcrouter');
expect(records[0].source).toEqual('local');
});
tap.test('migration runner rematerializes source-profile-backed route security', async () => {
const profiles: Array<Record<string, any>> = [
{
+2 -2
View File
@@ -54,13 +54,13 @@ const makeApiTokenManager = (
for (const policyScope of storedToken.policy?.scopes || []) {
scopes.add(policyScope);
}
const compatibilityAliases: Partial<Record<TScope, TScope[]>> = {
const equivalentScopes: Partial<Record<TScope, TScope[]>> = {
'gateway-clients:read': ['workhosters:read'],
'gateway-clients:write': ['workhosters:write'],
'workhosters:read': ['gateway-clients:read'],
'workhosters:write': ['gateway-clients:write'],
};
return scopes.has(scope) || Boolean(compatibilityAliases[scope]?.some((alias) => scopes.has(alias)));
return scopes.has(scope) || Boolean(equivalentScopes[scope]?.some((alias) => scopes.has(alias)));
},
};
};
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.45.0',
version: '14.0.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
+3 -77
View File
@@ -1,14 +1,12 @@
import { logger } from '../logger.js';
import { AcmeConfigDoc } from '../db/documents/index.js';
import type { IDcRouterOptions } from '../classes.dcrouter.js';
import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
/**
* AcmeConfigManager — owns the singleton ACME configuration in the DB.
*
* Lifecycle:
* - `start()` — loads from the DB; if empty, seeds from legacy constructor
* fields (`tls.contactEmail`, `smartProxyConfig.acme.*`) on first boot.
* - `start()` — loads the DB-backed singleton configuration.
* - `getConfig()` — returns the in-memory cached `IAcmeConfig` (or null)
* - `updateConfig(args, updatedBy)` — upserts and refreshes the cache
*
@@ -20,32 +18,12 @@ import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
export class AcmeConfigManager {
private cached: IAcmeConfig | null = null;
constructor(private options: IDcRouterOptions) {}
public async start(): Promise<void> {
logger.log('info', 'AcmeConfigManager: starting');
let doc = await AcmeConfigDoc.load();
const doc = await AcmeConfigDoc.load();
if (!doc) {
// First-boot path: seed from legacy constructor fields if present.
const seed = this.deriveSeedFromOptions();
if (seed) {
doc = await this.createSeedDoc(seed);
logger.log(
'info',
`AcmeConfigManager: seeded from constructor legacy fields (accountEmail=${seed.accountEmail}, useProduction=${seed.useProduction})`,
);
} else {
logger.log(
'info',
'AcmeConfigManager: no AcmeConfig in DB and no legacy constructor fields — ACME disabled until configured via Domains > Certificates > Settings.',
);
}
} else if (this.deriveSeedFromOptions()) {
logger.log(
'warn',
'AcmeConfigManager: ignoring constructor tls.contactEmail / smartProxyConfig.acme — DB already has AcmeConfigDoc. Manage via Domains > Certificates > Settings.',
);
logger.log('info', 'AcmeConfigManager: no AcmeConfig in DB — ACME disabled until configured via Domains > Certificates > Settings.');
}
this.cached = doc ? this.toPlain(doc) : null;
@@ -116,58 +94,6 @@ export class AcmeConfigManager {
// Internal helpers
// ==========================================================================
/**
* Build a seed object from the legacy constructor fields. Returns null
* if the user has not provided any of them.
*
* Supports BOTH `tls.contactEmail` (short form) and `smartProxyConfig.acme`
* (full form). `smartProxyConfig.acme` wins when both are present.
*/
private deriveSeedFromOptions(): Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'> | null {
const acme = this.options.smartProxyConfig?.acme;
const tls = this.options.tls;
// Prefer the explicit smartProxyConfig.acme block if present.
if (acme?.accountEmail) {
return {
accountEmail: acme.accountEmail,
enabled: acme.enabled !== false,
useProduction: acme.useProduction !== false,
autoRenew: acme.autoRenew !== false,
renewThresholdDays: acme.renewThresholdDays ?? 30,
};
}
// Fall back to the short tls.contactEmail form.
if (tls?.contactEmail) {
return {
accountEmail: tls.contactEmail,
enabled: true,
useProduction: true,
autoRenew: true,
renewThresholdDays: 30,
};
}
return null;
}
private async createSeedDoc(
seed: Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'>,
): Promise<AcmeConfigDoc> {
const doc = new AcmeConfigDoc();
doc.configId = 'acme-config';
doc.accountEmail = seed.accountEmail;
doc.enabled = seed.enabled;
doc.useProduction = seed.useProduction;
doc.autoRenew = seed.autoRenew;
doc.renewThresholdDays = seed.renewThresholdDays;
doc.updatedAt = Date.now();
doc.updatedBy = 'seed';
await doc.save();
return doc;
}
private toPlain(doc: AcmeConfigDoc): IAcmeConfig {
return {
accountEmail: doc.accountEmail,
+7 -27
View File
@@ -454,14 +454,13 @@ export class DcRouter {
// AcmeConfigManager: optional, depends on DcRouterDb — owns the singleton
// ACME configuration (accountEmail, useProduction, etc.). Must run before
// SmartProxy so setupSmartProxy() can read the ACME config from the DB.
// On first boot, seeds from legacy `tls.contactEmail` / `smartProxyConfig.acme`.
if (this.options.dbConfig?.enabled !== false) {
this.serviceManager.addService(
new plugins.taskbuffer.Service('AcmeConfigManager')
.optional()
.dependsOn('DcRouterDb')
.withStart(async () => {
this.acmeConfigManager = new AcmeConfigManager(this.options);
this.acmeConfigManager = new AcmeConfigManager();
await this.acmeConfigManager.start();
})
.withStop(async () => {
@@ -813,7 +812,7 @@ export class DcRouter {
?? false;
}
private getRemoteIngressHubSettingsLegacySeed(): TRemoteIngressHubSettingsUpdate {
private getRemoteIngressHubSettingsMigrationSeed(): TRemoteIngressHubSettingsUpdate {
const remoteIngressConfig = this.options.remoteIngressConfig;
const seed: TRemoteIngressHubSettingsUpdate = {};
if (remoteIngressConfig?.enabled !== undefined) {
@@ -831,7 +830,7 @@ export class DcRouter {
return seed;
}
private getEmailSettingsLegacySeed(): IEmailServerSettingsSeed {
private getEmailSettingsMigrationSeed(): IEmailServerSettingsSeed {
const seed: IEmailServerSettingsSeed = {};
if (this.options.emailConfig) {
seed.enabled = true;
@@ -1106,8 +1105,8 @@ export class DcRouter {
// Run any pending data migrations before anything else reads from the DB.
// This must complete before ConfigManagers loads profiles.
const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version, {
remoteIngressHubSettings: this.getRemoteIngressHubSettingsLegacySeed(),
emailServerSettings: this.getEmailSettingsLegacySeed(),
remoteIngressHubSettings: this.getRemoteIngressHubSettingsMigrationSeed(),
emailServerSettings: this.getEmailSettingsMigrationSeed(),
});
const migrationResult = await migration.run();
if (migrationResult.stepsApplied.length > 0) {
@@ -1972,28 +1971,9 @@ export class DcRouter {
465: 10465 // SMTPS
};
// Transform domains if they are provided as strings
let transformedDomains = this.options.emailConfig.domains;
if (transformedDomains && transformedDomains.length > 0) {
// Check if domains are strings (for backward compatibility)
if (typeof transformedDomains[0] === 'string') {
transformedDomains = (transformedDomains as any).map((domain: string) => ({
domain,
dnsMode: 'external-dns' as const,
dkim: {
selector: 'default',
keySize: 2048,
rotateKeys: false,
rotationInterval: 90
}
}));
}
}
// Create config with mapped ports
const emailConfig: IUnifiedEmailServerOptions = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
...this.options.emailConfig,
domains: transformedDomains,
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
persistRoutes: this.options.emailConfig.persistRoutes ?? false,
queue: {
@@ -2363,8 +2343,8 @@ export class DcRouter {
// Ensure DKIM keys exist for internal-dns domains before generating records.
await this.initializeDkimForEmailDomains();
// Generate DKIM records directly from smartmta instead of scanning legacy JSON files.
const dkimRecords = await this.loadDkimRecords();
// Generate DKIM records directly from smartmta.
const dkimRecords = await this.loadDkimRecords();
// Combine all records: authoritative, email, DKIM, and user-defined
const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
+2 -2
View File
@@ -111,13 +111,13 @@ export class ApiTokenManager {
const scopes = new Set<TApiTokenScope>([...token.scopes, ...(token.policy?.scopes || [])]);
if (scopes.has(scope)) return true;
const compatibilityAliases: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
const equivalentScopes: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
'gateway-clients:read': ['workhosters:read'],
'gateway-clients:write': ['workhosters:write'],
'workhosters:read': ['gateway-clients:read'],
'workhosters:write': ['gateway-clients:write'],
};
return Boolean(compatibilityAliases[scope]?.some((alias) => scopes.has(alias)));
return Boolean(equivalentScopes[scope]?.some((alias) => scopes.has(alias)));
}
/**
+1 -3
View File
@@ -8,9 +8,7 @@ const getDb = () => DcRouterDb.getInstance().getDb();
* keyed on the fixed `configId = 'acme-config'` following the
* `VpnServerKeysDoc` pattern.
*
* Replaces the legacy `tls.contactEmail` and `smartProxyConfig.acme.*`
* constructor fields. Managed via the OpsServer UI at
* **Domains > Certificates > Settings**.
* Managed via the OpsServer UI at **Domains > Certificates > Settings**.
*/
@plugins.smartdata.Collection(() => getDb())
export class AcmeConfigDoc extends plugins.smartdata.SmartDataDbDoc<AcmeConfigDoc, AcmeConfigDoc> {
-103
View File
@@ -24,7 +24,6 @@ import type {
*
* Responsibilities:
* - Load Domain/DnsRecord docs from the DB on start
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
* - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
* - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
* smartdns, provider domains hit the provider API)
@@ -53,13 +52,8 @@ export class DnsManager {
// Lifecycle
// ==========================================================================
/**
* Called from DcRouter after DcRouterDb is up. Performs first-boot seeding
* from legacy constructor config if (and only if) the DB is empty.
*/
public async start(): Promise<void> {
logger.log('info', 'DnsManager: starting');
await this.seedFromConstructorConfigIfEmpty();
}
public async stop(): Promise<void> {
@@ -77,103 +71,6 @@ export class DnsManager {
await this.applyDcrouterDomainsToDnsServer();
}
// ==========================================================================
// First-boot seeding
// ==========================================================================
/**
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
* seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
* local (`record.source: 'local'`) records. On subsequent boots (DB has
* entries), constructor config is ignored with a warning.
*/
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
const existingDomains = await DomainDoc.findAll();
const hasLegacyConfig =
(this.options.dnsScopes && this.options.dnsScopes.length > 0) ||
(this.options.dnsRecords && this.options.dnsRecords.length > 0);
if (existingDomains.length > 0) {
if (hasLegacyConfig) {
logger.log(
'warn',
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords constructor config. ' +
'dnsNsDomains is still required for nameserver and DoH bootstrap unless that moves into DB-backed config.',
);
}
return;
}
if (!hasLegacyConfig) {
return;
}
logger.log('info', 'DnsManager: seeding DB from legacy constructor DNS config');
const now = Date.now();
const seededDomains = new Map<string, DomainDoc>();
// Create one DomainDoc per dnsScope (these are the authoritative zones)
for (const scope of this.options.dnsScopes ?? []) {
const domain = new DomainDoc();
domain.id = plugins.uuid.v4();
domain.name = scope.toLowerCase();
domain.source = 'dcrouter';
domain.authoritative = true;
domain.createdAt = now;
domain.updatedAt = now;
domain.createdBy = 'seed';
await domain.save();
seededDomains.set(domain.name, domain);
logger.log('info', `DnsManager: seeded DomainDoc for ${domain.name}`);
}
// Map each legacy dnsRecord to its parent DomainDoc
for (const rec of this.options.dnsRecords ?? []) {
const parent = this.findParentDomain(rec.name, seededDomains);
if (!parent) {
logger.log(
'warn',
`DnsManager: legacy dnsRecord '${rec.name}' has no matching dnsScope — skipping seed`,
);
continue;
}
const record = new DnsRecordDoc();
record.id = plugins.uuid.v4();
record.domainId = parent.id;
record.name = rec.name.toLowerCase();
record.type = rec.type as TDnsRecordType;
record.value = rec.value;
record.ttl = rec.ttl ?? 300;
record.source = 'local';
record.createdAt = now;
record.updatedAt = now;
record.createdBy = 'seed';
await record.save();
}
logger.log(
'info',
`DnsManager: seeded ${seededDomains.size} domain(s) and ${this.options.dnsRecords?.length ?? 0} record(s) from legacy config`,
);
}
private findParentDomain(
recordName: string,
domains: Map<string, DomainDoc>,
): DomainDoc | null {
const lower = recordName.toLowerCase().replace(/^\*\./, '');
let candidate: DomainDoc | null = null;
for (const [name, doc] of domains) {
if (lower === name || lower.endsWith(`.${name}`)) {
if (!candidate || name.length > candidate.name.length) {
candidate = doc;
}
}
}
return candidate;
}
// ==========================================================================
// DcRouter-hosted domain DnsServer wiring
// ==========================================================================
@@ -61,17 +61,6 @@ export class CertificateHandler {
)
);
// Legacy route-based reprovision (backward compat)
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
'reprovisionCertificate',
async (dataArg) => {
await this.requireAuth(dataArg, 'certificates:write');
return this.reprovisionCertificateByRoute(dataArg.routeName);
}
)
);
// Domain-based reprovision (preferred)
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
@@ -336,42 +325,6 @@ export class CertificateHandler {
return summary;
}
/**
* Legacy route-based reprovisioning. Kept for backward compatibility with
* older clients that send `reprovisionCertificate` typed-requests.
*
* Like reprovisionCertificateDomain, this triggers the full route apply
* pipeline rather than smartProxy.provisionCertificate(routeName) — which
* is a no-op when certProvisionFunction is set (Rust ACME disabled).
*/
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
const dcRouter = this.opsServerRef.dcRouterRef;
const smartProxy = dcRouter.smartProxy;
if (!smartProxy) {
return { success: false, message: 'SmartProxy is not running' };
}
// Clear event-based status for domains in this route so the
// certificate-issued event can refresh them
for (const [domain, entry] of dcRouter.certificateStatusMap) {
if (entry.routeNames.includes(routeName)) {
dcRouter.certificateStatusMap.delete(domain);
}
}
try {
if (dcRouter.routeConfigManager) {
await dcRouter.routeConfigManager.applyRoutes();
} else {
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
}
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
} catch (err: unknown) {
return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
}
}
/**
* Domain-based reprovisioning — clears backoff first, refreshes the smartacme
* cert (when forceRenew is set), then re-applies routes so the running Rust
+10 -11
View File
@@ -59,15 +59,15 @@ export class ConfigHandler {
};
// --- SmartProxy ---
const acmeConfig = dcRouter.acmeConfigManager?.getConfig();
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
if (opts.smartProxyConfig?.acme) {
const acme = opts.smartProxyConfig.acme;
if (acmeConfig) {
acmeInfo = {
enabled: acme.enabled !== false,
accountEmail: acme.accountEmail || '',
useProduction: acme.useProduction !== false,
autoRenew: acme.autoRenew !== false,
renewThresholdDays: acme.renewThresholdDays || 30,
enabled: acmeConfig.enabled,
accountEmail: acmeConfig.accountEmail,
useProduction: acmeConfig.useProduction,
autoRenew: acmeConfig.autoRenew,
renewThresholdDays: acmeConfig.renewThresholdDays,
};
}
@@ -127,8 +127,7 @@ export class ConfigHandler {
ttl: r.ttl,
}));
// dnsChallenge: true when at least one DnsProviderDoc exists in the DB
// (replaces the legacy `dnsChallenge.cloudflareApiKey` constructor field).
// dnsChallenge: true when at least one DnsProviderDoc exists in the DB.
let dnsChallengeEnabled = false;
try {
dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAnyManagedDomain()) ?? false;
@@ -150,12 +149,12 @@ export class ConfigHandler {
let tlsSource: 'acme' | 'static' | 'none' = 'none';
if (opts.tls?.certPath && opts.tls?.keyPath) {
tlsSource = 'static';
} else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
} else if (acmeConfig?.enabled) {
tlsSource = 'acme';
}
const tls: interfaces.requests.IConfigData['tls'] = {
contactEmail: opts.tls?.contactEmail || opts.smartProxyConfig?.acme?.accountEmail || null,
contactEmail: acmeConfig?.accountEmail || null,
domain: opts.tls?.domain || null,
source: tlsSource,
certPath: opts.tls?.certPath || null,
@@ -66,7 +66,7 @@ export class EmailSettingsHandler {
routeCount: emailConfig?.routes?.length || 0,
authUserCount: emailConfig?.auth?.users?.length || 0,
updatedAt: 0,
updatedBy: 'legacy-options',
updatedBy: 'runtime-options',
};
}
}
+1 -3
View File
@@ -1,9 +1,7 @@
/**
* ACME configuration for automated TLS certificate issuance via Let's Encrypt.
*
* Persisted as a singleton `AcmeConfigDoc` in the DcRouterDb. Replaces the
* legacy constructor fields `tls.contactEmail` / `smartProxyConfig.acme.*`
* which are now seed-only (used once on first boot if the DB is empty).
* Persisted as a singleton `AcmeConfigDoc` in the DcRouterDb.
*
* Managed via the OpsServer UI at **Domains > Certificates > Settings**.
*/
-18
View File
@@ -44,24 +44,6 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
};
}
// Legacy route-based reprovision (kept for backward compat)
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ReprovisionCertificate
> {
method: 'reprovisionCertificate';
request: {
identity?: authInterfaces.IIdentity;
apiToken?: string;
routeName: string;
};
response: {
success: boolean;
message?: string;
};
}
// Domain-based reprovision (preferred)
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ReprovisionCertificateDomain
+2 -2
View File
@@ -541,7 +541,7 @@ export async function createMigrationRunner(
.from('13.1.0').to('13.8.1')
.description('Rename DomainDoc.source value from "manual" to "dcrouter"')
.up(async (ctx) => {
const collection = ctx.mongo!.collection('domaindoc');
const collection = ctx.mongo!.collection('DomainDoc');
const result = await collection.updateMany(
{ source: 'manual' },
{ $set: { source: 'dcrouter' } },
@@ -555,7 +555,7 @@ export async function createMigrationRunner(
.from('13.8.1').to('13.8.2')
.description('Rename DnsRecordDoc.source value from "manual" to "local"')
.up(async (ctx) => {
const collection = ctx.mongo!.collection('dnsrecorddoc');
const collection = ctx.mongo!.collection('DnsRecordDoc');
const result = await collection.updateMany(
{ source: 'manual' },
{ $set: { source: 'local' } },
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.45.0',
version: '14.0.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}