fix(dcrouter): persist proxy certificate validity dates and improve certificate status initialization

This commit is contained in:
2026-02-16 02:50:25 +00:00
parent 4e32745a8f
commit de0b7d1fe0
7 changed files with 78 additions and 29 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## 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) ## 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 Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler

View File

@@ -36,7 +36,7 @@
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@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/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.1", "@push.rocks/smartdns": "^7.8.1",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",

10
pnpm-lock.yaml generated
View File

@@ -36,8 +36,8 @@ importers:
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
'@push.rocks/smartacme': '@push.rocks/smartacme':
specifier: ^9.0.0 specifier: ^9.1.3
version: 9.1.2(socks@2.8.7) version: 9.1.3(socks@2.8.7)
'@push.rocks/smartdata': '@push.rocks/smartdata':
specifier: ^7.0.15 specifier: ^7.0.15
version: 7.0.15(socks@2.8.7) version: 7.0.15(socks@2.8.7)
@@ -852,8 +852,8 @@ packages:
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartacme@9.1.2': '@push.rocks/smartacme@9.1.3':
resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==} resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==}
'@push.rocks/smartarchive@4.2.4': '@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -5782,7 +5782,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@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: dependencies:
'@apiclient.xyz/cloudflare': 7.1.0 '@apiclient.xyz/cloudflare': 7.1.0
'@peculiar/x509': 1.14.3 '@peculiar/x509': 1.14.3

View File

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

View File

@@ -445,8 +445,8 @@ export class DcRouter {
if (routes.length > 0 || this.options.smartProxyConfig) { if (routes.length > 0 || this.options.smartProxyConfig) {
console.log('Setting up SmartProxy with combined configuration'); console.log('Setting up SmartProxy with combined configuration');
// Track domains loaded from cert store so we can populate certificateStatusMap after start // Track cert entries loaded from cert store so we can populate certificateStatusMap after start
const loadedCertDomains: string[] = []; const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
// Create SmartProxy configuration // Create SmartProxy configuration
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
@@ -461,14 +461,21 @@ export class DcRouter {
const data = await this.storageManager.getJSON(key); const data = await this.storageManager.getJSON(key);
if (data) { if (data) {
certs.push(data); certs.push(data);
loadedCertDomains.push(data.domain); loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
} }
} }
return certs; return certs;
}, },
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => { 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}`, { await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
domain, publicKey, privateKey, ca, domain, publicKey, privateKey, ca, validUntil, validFrom,
}); });
}, },
remove: async (domain: string) => { remove: async (domain: string) => {
@@ -587,22 +594,46 @@ export class DcRouter {
console.log('[DcRouter] SmartProxy started successfully'); console.log('[DcRouter] SmartProxy started successfully');
// Populate certificateStatusMap for certs loaded from store at startup // Populate certificateStatusMap for certs loaded from store at startup
for (const domain of loadedCertDomains) { for (const entry of loadedCertEntries) {
if (!this.certificateStatusMap.has(domain)) { if (!this.certificateStatusMap.has(entry.domain)) {
const routeNames = this.findRouteNamesForDomain(domain); const routeNames = this.findRouteNamesForDomain(entry.domain);
let expiryDate: string | undefined; let expiryDate: string | undefined;
let issuedAt: string | undefined; let issuedAt: string | undefined;
try {
const cleanDomain = domain.replace(/^\*\.?/, ''); // Use validUntil/validFrom from stored proxy-certs data if available
const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`); if (entry.validUntil) {
if (certMeta?.validUntil) { expiryDate = new Date(entry.validUntil).toISOString();
expiryDate = new Date(certMeta.validUntil).toISOString(); }
} if (entry.validFrom) {
if (certMeta?.created) { issuedAt = new Date(entry.validFrom).toISOString();
issuedAt = new Date(certMeta.created).toISOString(); }
}
} catch { /* no metadata available */ } // Try SmartAcme /certs/ metadata as secondary source
this.certificateStatusMap.set(domain, { 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', status: 'valid',
routeNames, routeNames,
expiryDate, expiryDate,
@@ -611,8 +642,8 @@ export class DcRouter {
}); });
} }
} }
if (loadedCertDomains.length > 0) { if (loadedCertEntries.length > 0) {
console.log(`[DcRouter] Populated certificate status for ${loadedCertDomains.length} store-loaded domain(s)`); console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
} }
console.log(`SmartProxy started with ${routes.length} routes`); console.log(`SmartProxy started with ${routes.length} routes`);

View File

@@ -167,8 +167,16 @@ export class CertificateHandler {
issuedAt = new Date(certData.created).toISOString(); issuedAt = new Date(certData.created).toISOString();
} }
issuer = 'smartacme-dns-01'; 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) { } else if (certData) {
// certStore has the cert (no expiry metadata) — it's loaded and serving
status = 'valid'; status = 'valid';
issuer = 'cert-store'; issuer = 'cert-store';
} }

View File

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