Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb472f353c | |||
| 090bd747e1 | |||
| 4d77a94bbb | |||
| 7f5284b10f | |||
| 9cd5db2d81 | |||
| de0b7d1fe0 |
25
changelog.md
25
changelog.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-16 - 6.3.0 - feat(dcrouter)
|
||||
add configurable baseDir and centralized path resolution; use resolved data paths for storage, cache and DNS
|
||||
|
||||
- Introduce IDcRouterOptions.baseDir to allow configuring base directory for dcrouter data (defaults to ~/.serve.zone/dcrouter).
|
||||
- Add DcRouter.resolvedPaths and resolvePaths(baseDir) in ts/paths.ts to centralize computation of dcrouterHomeDir, dataDir, defaultTsmDbPath, defaultStoragePath and dnsRecordsDir.
|
||||
- Use resolvedPaths throughout DcRouter: default filesystem storage fsPath, CacheDb storagePath, and DNS records loading now reference resolved paths.
|
||||
- Replace ensureDirectories() behavior with ensureDataDirectories(resolvedPaths) to only create data-related directories; keep legacy ensureDirectories wrapper delegating to the new function.
|
||||
- Simplify paths module by removing unused legacy path constants and adding a focused API for path resolution and directory creation.
|
||||
- Remove an unused import (paths) in contentscanner, cleaning up imports.
|
||||
|
||||
## 2026-02-16 - 6.2.4 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.5.0
|
||||
|
||||
- Updated @push.rocks/smartproxy from ^25.4.0 to ^25.5.0 in package.json
|
||||
|
||||
## 2026-02-16 - 6.2.3 - fix(dcrouter)
|
||||
persist proxy certificate validity dates and improve certificate status initialization
|
||||
|
||||
- Bump @push.rocks/smartacme dependency from ^9.0.0 to ^9.1.3
|
||||
- Store validFrom and validUntil alongside proxy cert entries (/proxy-certs) when saving, extracting values by parsing PEM where possible
|
||||
- Use stored cert entries (domain, publicKey, validUntil, validFrom) to populate certificateStatusMap at startup
|
||||
- Fallback to SmartAcme /certs/ metadata and finally to parsing X.509 from stored PEM to determine expiry/issuedAt when initializing status
|
||||
- Update opsserver certificate handler to parse publicKey PEM from cert-store and set expiry/issuedAt and issuer accordingly
|
||||
- Adjust variable names and logging to reflect stored cert entry usage
|
||||
|
||||
## 2026-02-16 - 6.2.2 - fix(certs)
|
||||
Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "6.2.2",
|
||||
"version": "6.3.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^9.0.0",
|
||||
"@push.rocks/smartacme": "^9.1.3",
|
||||
"@push.rocks/smartdata": "^7.0.15",
|
||||
"@push.rocks/smartdns": "^7.8.1",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^25.4.0",
|
||||
"@push.rocks/smartproxy": "^25.5.0",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.2(socks@2.8.7)
|
||||
specifier: ^9.1.3
|
||||
version: 9.1.3(socks@2.8.7)
|
||||
'@push.rocks/smartdata':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15(socks@2.8.7)
|
||||
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^25.4.0
|
||||
version: 25.4.0
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -852,8 +852,8 @@ packages:
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||
|
||||
'@push.rocks/smartacme@9.1.2':
|
||||
resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==}
|
||||
'@push.rocks/smartacme@9.1.3':
|
||||
resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==}
|
||||
|
||||
'@push.rocks/smartarchive@4.2.4':
|
||||
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
||||
@@ -1031,8 +1031,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@25.4.0':
|
||||
resolution: {integrity: sha512-aU7ySk/2llRs6hcIGrl4gjuXsJOmLuVv952ys4H1yZyZSdPx7G8m5gJl6RxB5Rp8GzM2YaKUvCp00apcGcnEfw==}
|
||||
'@push.rocks/smartproxy@25.5.0':
|
||||
resolution: {integrity: sha512-ePjxuwplEWpvvK0Xnb3q/8BVmE4xrBJl1mSoKBcZOzizF2T6ZmwuQKIvjnDJ13Q/KHLSIqMFS61CWVJHwtOUfA==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -5782,7 +5782,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.1.11
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
'@push.rocks/smartacme@9.1.2(socks@2.8.7)':
|
||||
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@apiclient.xyz/cloudflare': 7.1.0
|
||||
'@peculiar/x509': 1.14.3
|
||||
@@ -6369,7 +6369,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@25.4.0':
|
||||
'@push.rocks/smartproxy@25.5.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.1.11
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.2.2',
|
||||
version: '6.3.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ import { MetricsManager } from './monitoring/index.js';
|
||||
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/**
|
||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||
baseDir?: string;
|
||||
|
||||
/**
|
||||
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
|
||||
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
|
||||
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
|
||||
*/
|
||||
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
|
||||
|
||||
@@ -170,6 +173,7 @@ export interface PortProxyRuleContext {
|
||||
|
||||
export class DcRouter {
|
||||
public options: IDcRouterOptions;
|
||||
public resolvedPaths: ReturnType<typeof paths.resolvePaths>;
|
||||
|
||||
// Core services
|
||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||
@@ -210,10 +214,13 @@ export class DcRouter {
|
||||
...optionsArg
|
||||
};
|
||||
|
||||
// Resolve all data paths from baseDir
|
||||
this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
|
||||
|
||||
// Default storage to filesystem if not configured
|
||||
if (!this.options.storage) {
|
||||
this.options.storage = {
|
||||
fsPath: plugins.path.join(paths.dcrouterHomeDir, 'storage'),
|
||||
fsPath: this.resolvedPaths.defaultStoragePath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,7 +379,7 @@ export class DcRouter {
|
||||
|
||||
// Initialize CacheDb singleton
|
||||
this.cacheDb = CacheDb.getInstance({
|
||||
storagePath: cacheConfig.storagePath || paths.defaultTsmDbPath,
|
||||
storagePath: cacheConfig.storagePath || this.resolvedPaths.defaultTsmDbPath,
|
||||
dbName: cacheConfig.dbName || 'dcrouter',
|
||||
debug: false,
|
||||
});
|
||||
@@ -445,8 +452,8 @@ export class DcRouter {
|
||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||
console.log('Setting up SmartProxy with combined configuration');
|
||||
|
||||
// Track domains loaded from cert store so we can populate certificateStatusMap after start
|
||||
const loadedCertDomains: string[] = [];
|
||||
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||
|
||||
// Create SmartProxy configuration
|
||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||
@@ -461,14 +468,21 @@ export class DcRouter {
|
||||
const data = await this.storageManager.getJSON(key);
|
||||
if (data) {
|
||||
certs.push(data);
|
||||
loadedCertDomains.push(data.domain);
|
||||
loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
|
||||
}
|
||||
}
|
||||
return certs;
|
||||
},
|
||||
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
|
||||
let validUntil: number | undefined;
|
||||
let validFrom: number | undefined;
|
||||
try {
|
||||
const x509 = new plugins.crypto.X509Certificate(publicKey);
|
||||
validUntil = new Date(x509.validTo).getTime();
|
||||
validFrom = new Date(x509.validFrom).getTime();
|
||||
} catch { /* PEM parsing failed */ }
|
||||
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
|
||||
domain, publicKey, privateKey, ca,
|
||||
domain, publicKey, privateKey, ca, validUntil, validFrom,
|
||||
});
|
||||
},
|
||||
remove: async (domain: string) => {
|
||||
@@ -587,22 +601,46 @@ export class DcRouter {
|
||||
console.log('[DcRouter] SmartProxy started successfully');
|
||||
|
||||
// Populate certificateStatusMap for certs loaded from store at startup
|
||||
for (const domain of loadedCertDomains) {
|
||||
if (!this.certificateStatusMap.has(domain)) {
|
||||
const routeNames = this.findRouteNamesForDomain(domain);
|
||||
for (const entry of loadedCertEntries) {
|
||||
if (!this.certificateStatusMap.has(entry.domain)) {
|
||||
const routeNames = this.findRouteNamesForDomain(entry.domain);
|
||||
let expiryDate: string | undefined;
|
||||
let issuedAt: string | undefined;
|
||||
try {
|
||||
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||
const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||
if (certMeta?.validUntil) {
|
||||
expiryDate = new Date(certMeta.validUntil).toISOString();
|
||||
}
|
||||
if (certMeta?.created) {
|
||||
issuedAt = new Date(certMeta.created).toISOString();
|
||||
}
|
||||
} catch { /* no metadata available */ }
|
||||
this.certificateStatusMap.set(domain, {
|
||||
|
||||
// Use validUntil/validFrom from stored proxy-certs data if available
|
||||
if (entry.validUntil) {
|
||||
expiryDate = new Date(entry.validUntil).toISOString();
|
||||
}
|
||||
if (entry.validFrom) {
|
||||
issuedAt = new Date(entry.validFrom).toISOString();
|
||||
}
|
||||
|
||||
// Try SmartAcme /certs/ metadata as secondary source
|
||||
if (!expiryDate) {
|
||||
try {
|
||||
const cleanDomain = entry.domain.replace(/^\*\.?/, '');
|
||||
const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||
if (certMeta?.validUntil) {
|
||||
expiryDate = new Date(certMeta.validUntil).toISOString();
|
||||
}
|
||||
if (certMeta?.created && !issuedAt) {
|
||||
issuedAt = new Date(certMeta.created).toISOString();
|
||||
}
|
||||
} catch { /* no metadata available */ }
|
||||
}
|
||||
|
||||
// Fallback: parse X509 from PEM to get expiry
|
||||
if (!expiryDate && entry.publicKey) {
|
||||
try {
|
||||
const x509 = new plugins.crypto.X509Certificate(entry.publicKey);
|
||||
expiryDate = new Date(x509.validTo).toISOString();
|
||||
if (!issuedAt) {
|
||||
issuedAt = new Date(x509.validFrom).toISOString();
|
||||
}
|
||||
} catch { /* PEM parsing failed */ }
|
||||
}
|
||||
|
||||
this.certificateStatusMap.set(entry.domain, {
|
||||
status: 'valid',
|
||||
routeNames,
|
||||
expiryDate,
|
||||
@@ -611,8 +649,8 @@ export class DcRouter {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (loadedCertDomains.length > 0) {
|
||||
console.log(`[DcRouter] Populated certificate status for ${loadedCertDomains.length} store-loaded domain(s)`);
|
||||
if (loadedCertEntries.length > 0) {
|
||||
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||
}
|
||||
|
||||
console.log(`SmartProxy started with ${routes.length} routes`);
|
||||
@@ -1275,7 +1313,7 @@ export class DcRouter {
|
||||
|
||||
try {
|
||||
// Ensure paths are imported
|
||||
const dnsDir = paths.dnsRecordsDir;
|
||||
const dnsDir = this.resolvedPaths.dnsRecordsDir;
|
||||
|
||||
// Check if directory exists
|
||||
if (!plugins.fs.existsSync(dnsDir)) {
|
||||
@@ -1339,7 +1377,7 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
// Ensure necessary directories exist
|
||||
paths.ensureDirectories();
|
||||
paths.ensureDataDirectories(this.resolvedPaths);
|
||||
|
||||
// Generate DKIM keys for each email domain
|
||||
for (const domainConfig of this.options.emailConfig.domains) {
|
||||
|
||||
@@ -167,8 +167,16 @@ export class CertificateHandler {
|
||||
issuedAt = new Date(certData.created).toISOString();
|
||||
}
|
||||
issuer = 'smartacme-dns-01';
|
||||
} else if (certData?.publicKey) {
|
||||
// certStore has the cert — parse PEM for expiry
|
||||
try {
|
||||
const x509 = new plugins.crypto.X509Certificate(certData.publicKey);
|
||||
expiryDate = new Date(x509.validTo).toISOString();
|
||||
issuedAt = new Date(x509.validFrom).toISOString();
|
||||
} catch { /* PEM parsing failed */ }
|
||||
status = 'valid';
|
||||
issuer = 'cert-store';
|
||||
} else if (certData) {
|
||||
// certStore has the cert (no expiry metadata) — it's loaded and serving
|
||||
status = 'valid';
|
||||
issuer = 'cert-store';
|
||||
}
|
||||
|
||||
59
ts/paths.ts
59
ts/paths.ts
@@ -1,7 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// Base directories
|
||||
export const baseDir = process.cwd();
|
||||
// Code/asset paths (not affected by baseDir)
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
@@ -20,35 +19,37 @@ export const dataDir = process.env.DATA_DIR
|
||||
// Default TsmDB path for CacheDb
|
||||
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
||||
|
||||
// MTA directories
|
||||
export const keysDir = plugins.path.join(dataDir, 'keys');
|
||||
// DNS records directory (only surviving MTA directory reference)
|
||||
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
||||
export const sentEmailsDir = plugins.path.join(dataDir, 'emails', 'sent');
|
||||
export const receivedEmailsDir = plugins.path.join(dataDir, 'emails', 'received');
|
||||
export const failedEmailsDir = plugins.path.join(dataDir, 'emails', 'failed'); // For failed emails
|
||||
export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
|
||||
|
||||
// Email template directories
|
||||
export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
|
||||
export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
|
||||
/**
|
||||
* Resolve all data paths from a given baseDir.
|
||||
* When no baseDir is provided, falls back to ~/.serve.zone/dcrouter.
|
||||
* Specific overrides (e.g. DATA_DIR env) take precedence.
|
||||
*/
|
||||
export function resolvePaths(baseDir?: string) {
|
||||
const root = baseDir ?? plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
|
||||
const resolvedDataDir = process.env.DATA_DIR ?? plugins.path.join(root, 'data');
|
||||
return {
|
||||
dcrouterHomeDir: root,
|
||||
dataDir: resolvedDataDir,
|
||||
defaultTsmDbPath: plugins.path.join(root, 'tsmdb'),
|
||||
defaultStoragePath: plugins.path.join(root, 'storage'),
|
||||
dnsRecordsDir: plugins.path.join(resolvedDataDir, 'dns'),
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration path
|
||||
export const configPath = process.env.CONFIG_PATH
|
||||
? process.env.CONFIG_PATH
|
||||
: plugins.path.join(baseDir, 'config.json');
|
||||
/**
|
||||
* Ensure only the data directories that are actually used exist.
|
||||
*/
|
||||
export function ensureDataDirectories(resolvedPaths: ReturnType<typeof resolvePaths>) {
|
||||
plugins.fsUtils.ensureDirSync(resolvedPaths.dataDir);
|
||||
plugins.fsUtils.ensureDirSync(resolvedPaths.dnsRecordsDir);
|
||||
}
|
||||
|
||||
// Create directories if they don't exist
|
||||
/**
|
||||
* Legacy wrapper — delegates to ensureDataDirectories with module-level defaults.
|
||||
*/
|
||||
export function ensureDirectories() {
|
||||
// Ensure data directories
|
||||
plugins.fsUtils.ensureDirSync(dataDir);
|
||||
plugins.fsUtils.ensureDirSync(keysDir);
|
||||
plugins.fsUtils.ensureDirSync(dnsRecordsDir);
|
||||
plugins.fsUtils.ensureDirSync(sentEmailsDir);
|
||||
plugins.fsUtils.ensureDirSync(receivedEmailsDir);
|
||||
plugins.fsUtils.ensureDirSync(failedEmailsDir);
|
||||
plugins.fsUtils.ensureDirSync(logsDir);
|
||||
|
||||
// Ensure email template directories
|
||||
plugins.fsUtils.ensureDirSync(emailTemplatesDir);
|
||||
plugins.fsUtils.ensureDirSync(MtaAttachmentsDir);
|
||||
}
|
||||
ensureDataDirectories(resolvePaths());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { Email, type Core } from '@push.rocks/smartmta';
|
||||
type IAttachment = Core.IAttachment;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.2.2',
|
||||
version: '6.3.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user