Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d44528345 | |||
| 28a38252da | |||
| dfb268bbfc | |||
| 6532c7ff22 |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.5.0 - feat(certs)
|
||||||
|
persist ACME certificates in StorageManager, add storage-backed cert manager, default storage to filesystem, and improve certificate status reporting
|
||||||
|
|
||||||
|
- Add StorageBackedCertManager to persist SmartAcme certificates under /certs/ via StorageManager
|
||||||
|
- Default storage to filesystem path (dcrouterHomeDir/storage) when options.storage is not provided
|
||||||
|
- Wire SmartAcme to use StorageBackedCertManager and provide SmartProxy certStore handlers that load/save/remove certs under /proxy-certs/
|
||||||
|
- Ops server certificate handler reads persisted cert data to report expiry/issued dates and treats acme/provision-function routes with no cert data as provisioning
|
||||||
|
- Bump @push.rocks/smartproxy dependency to ^25.3.0
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.4.6 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy dependency to ^25.2.2
|
||||||
|
|
||||||
|
- Updated dependency @push.rocks/smartproxy: ^25.2.0 → ^25.2.2
|
||||||
|
- Change is a dependency-only patch update, no source code modifications
|
||||||
|
- Current package version is 5.4.5; recommend a patch release
|
||||||
|
|
||||||
## 2026-02-14 - 5.4.5 - fix(dcrouter)
|
## 2026-02-14 - 5.4.5 - fix(dcrouter)
|
||||||
bump patch for release pipeline consistency - no code changes
|
bump patch for release pipeline consistency - no code changes
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "5.4.5",
|
"version": "5.5.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.2.0",
|
"@push.rocks/smartproxy": "^25.3.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^25.2.0
|
specifier: ^25.3.0
|
||||||
version: 25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 25.3.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -1040,8 +1040,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.2.0':
|
'@push.rocks/smartproxy@25.3.0':
|
||||||
resolution: {integrity: sha512-cwqtfSI3QziyZOYXZuL4/jq1KHXQRVwGvimHcqhJDsl4cac9y7fM4gKHU4B3m2/2qaih1scP9FPGwlCCVFXR7Q==}
|
resolution: {integrity: sha512-ie0jP6dCSZFvrdRmlo5NTufA6AJeQdGsgVQv6M9okQ4IXBkm3LVN+u6t9T2nHalnopMJXLb+qAuq0Y2T5mxIJg==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -6441,7 +6441,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
'@push.rocks/smartproxy@25.3.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.5',
|
version: '5.5.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
// Import storage manager
|
// Import storage manager
|
||||||
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
||||||
|
import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
|
||||||
// Import cache system
|
// Import cache system
|
||||||
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
|
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
|
||||||
|
|
||||||
@@ -205,6 +206,13 @@ export class DcRouter {
|
|||||||
...optionsArg
|
...optionsArg
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Default storage to filesystem if not configured
|
||||||
|
if (!this.options.storage) {
|
||||||
|
this.options.storage = {
|
||||||
|
fsPath: plugins.path.join(paths.dcrouterHomeDir, 'storage'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize storage manager
|
// Initialize storage manager
|
||||||
this.storageManager = new StorageManager(this.options.storage);
|
this.storageManager = new StorageManager(this.options.storage);
|
||||||
}
|
}
|
||||||
@@ -437,14 +445,33 @@ export class DcRouter {
|
|||||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||||
...this.options.smartProxyConfig,
|
...this.options.smartProxyConfig,
|
||||||
routes,
|
routes,
|
||||||
acme: acmeConfig
|
acme: acmeConfig,
|
||||||
|
certStore: {
|
||||||
|
loadAll: async () => {
|
||||||
|
const keys = await this.storageManager.list('/proxy-certs/');
|
||||||
|
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const data = await this.storageManager.getJSON(key);
|
||||||
|
if (data) certs.push(data);
|
||||||
|
}
|
||||||
|
return certs;
|
||||||
|
},
|
||||||
|
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
|
||||||
|
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
|
||||||
|
domain, publicKey, privateKey, ca,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: async (domain: string) => {
|
||||||
|
await this.storageManager.delete(`/proxy-certs/${domain}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||||
if (challengeHandlers.length > 0) {
|
if (challengeHandlers.length > 0) {
|
||||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
certManager: new StorageBackedCertManager(this.storageManager),
|
||||||
environment: 'production',
|
environment: 'production',
|
||||||
challengeHandlers: challengeHandlers,
|
challengeHandlers: challengeHandlers,
|
||||||
challengePriority: ['dns-01'],
|
challengePriority: ['dns-01'],
|
||||||
|
|||||||
46
ts/classes.storage-cert-manager.ts
Normal file
46
ts/classes.storage-cert-manager.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { StorageManager } from './storage/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ICertManager implementation backed by StorageManager.
|
||||||
|
* Persists SmartAcme certificates under a /certs/ key prefix so they
|
||||||
|
* survive process restarts without re-hitting ACME.
|
||||||
|
*/
|
||||||
|
export class StorageBackedCertManager implements plugins.smartacme.ICertManager {
|
||||||
|
private keyPrefix = '/certs/';
|
||||||
|
|
||||||
|
constructor(private storageManager: StorageManager) {}
|
||||||
|
|
||||||
|
async init(): Promise<void> {}
|
||||||
|
|
||||||
|
async retrieveCertificate(domainName: string): Promise<plugins.smartacme.Cert | null> {
|
||||||
|
const data = await this.storageManager.getJSON(this.keyPrefix + domainName);
|
||||||
|
if (!data) return null;
|
||||||
|
return new plugins.smartacme.Cert(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(this.keyPrefix + cert.domainName, {
|
||||||
|
id: cert.id,
|
||||||
|
domainName: cert.domainName,
|
||||||
|
created: cert.created,
|
||||||
|
privateKey: cert.privateKey,
|
||||||
|
publicKey: cert.publicKey,
|
||||||
|
csr: cert.csr,
|
||||||
|
validUntil: cert.validUntil,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCertificate(domainName: string): Promise<void> {
|
||||||
|
await this.storageManager.delete(this.keyPrefix + domainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
|
||||||
|
async wipe(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(this.keyPrefix);
|
||||||
|
for (const key of keys) {
|
||||||
|
await this.storageManager.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,22 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check persisted cert data from StorageManager
|
||||||
|
if (status === 'unknown' && routeDomains.length > 0) {
|
||||||
|
for (const domain of routeDomains) {
|
||||||
|
if (expiryDate) break;
|
||||||
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||||
|
const certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (certData?.validUntil) {
|
||||||
|
expiryDate = new Date(certData.validUntil).toISOString();
|
||||||
|
if (certData.created) {
|
||||||
|
issuedAt = new Date(certData.created).toISOString();
|
||||||
|
}
|
||||||
|
issuer = 'smartacme-dns-01';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
// Compute status from expiry date if we have one and status is still valid/unknown
|
||||||
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
||||||
const expiry = new Date(expiryDate);
|
const expiry = new Date(expiryDate);
|
||||||
@@ -124,6 +140,11 @@ export class CertificateHandler {
|
|||||||
status = 'valid';
|
status = 'valid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ACME/provision-function routes with no cert data are still provisioning
|
||||||
|
if (status === 'unknown' && (source === 'acme' || source === 'provision-function')) {
|
||||||
|
status = 'provisioning';
|
||||||
|
}
|
||||||
|
|
||||||
const canReprovision = source === 'acme' || source === 'provision-function';
|
const canReprovision = source === 'acme' || source === 'provision-function';
|
||||||
|
|
||||||
certificates.push({
|
certificates.push({
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.5',
|
version: '5.5.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user