diff --git a/changelog.md b/changelog.md
index 30bc729..25a04d9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,16 @@
# Changelog
+## 2026-02-16 - 6.1.0 - feat(certs)
+integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler
+
+- Bump dependency: @push.rocks/smartacme -> ^9.0.0
+- Add Certificate Management documentation, examples, and a new Certificates view in the OpsServer dashboard (status, source, expiry, backoff, oneβclick reprovision)
+- Integrate smartacme v9 features: per-domain deduplication, global concurrency control, account rate limiting, structured errors, and clean shutdown behavior
+- Introduce per-domain exponential backoff persisted via StorageManager (CertProvisionScheduler) and remove the older serial stagger queue (smartacme v9 handles concurrency/deduping)
+- Expose new typedrequest API methods: getCertificateOverview, reprovisionCertificate (legacy), reprovisionCertificateDomain (preferred)
+- DcRouter now surfaces smartAcme, certProvisionScheduler, and certificateStatusMap; cert provisioning paths call smartAcme directly and clear backoff on success
+- Docs updated to note parallel shutdown/cleanup of HTTP agents and DNS clients
+
## 2026-02-15 - 6.0.0 - BREAKING CHANGE(certs)
Introduce domain-centric certificate provisioning with per-domain exponential backoff and a staggered serial scheduler; add domain-based reprovision API and UI backoff display; change certificate overview API to be domain-first and include backoff info; bump related deps.
diff --git a/package.json b/package.json
index 8abae90..09b6589 100644
--- a/package.json
+++ b/package.json
@@ -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": "^8.0.0",
+ "@push.rocks/smartacme": "^9.0.0",
"@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.1",
"@push.rocks/smartfile": "^13.1.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d204424..a016952 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -36,8 +36,8 @@ importers:
specifier: ^6.1.3
version: 6.1.3
'@push.rocks/smartacme':
- specifier: ^8.0.0
- version: 8.0.0(socks@2.8.7)
+ specifier: ^9.0.0
+ version: 9.1.2(socks@2.8.7)
'@push.rocks/smartdata':
specifier: ^7.0.15
version: 7.0.15(socks@2.8.7)
@@ -154,9 +154,6 @@ packages:
peerDependencies:
'@push.rocks/smartserve': '>=1.1.0'
- '@apiclient.xyz/cloudflare@6.4.3':
- resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
-
'@apiclient.xyz/cloudflare@7.1.0':
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
@@ -651,9 +648,6 @@ packages:
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'}
- '@leichtgewicht/ip-codec@2.0.5':
- resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
-
'@lit-labs/ssr-dom-shim@1.5.1':
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
@@ -858,8 +852,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
- '@push.rocks/smartacme@8.0.0':
- resolution: {integrity: sha512-Oq+m+LX4IG0p4qCGZLEwa6UlMo5Hfq7paRjpREwQNsaGSKl23xsjsEJLxjxkePwaXnaIkHEwU/5MtrEkg2uKEQ==}
+ '@push.rocks/smartacme@9.1.2':
+ resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -901,9 +895,6 @@ packages:
'@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
- '@push.rocks/smartdns@6.2.2':
- resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
-
'@push.rocks/smartdns@7.8.1':
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
@@ -1100,6 +1091,9 @@ packages:
'@push.rocks/smarttime@4.1.1':
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
+ '@push.rocks/smarttime@4.2.3':
+ resolution: {integrity: sha512-8gMg8RUkrCG4p9NcEUZV7V6KpL24+jAMK02g7qyhfA6giz/JJWD0+8w8xjSR+G7qe16KVQ2y3RbvAL9TxmO36g==}
+
'@push.rocks/smartunique@3.0.9':
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
@@ -1128,6 +1122,9 @@ packages:
'@push.rocks/taskbuffer@4.2.0':
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
+ '@push.rocks/taskbuffer@6.1.2':
+ resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
+
'@push.rocks/webrequest@3.0.37':
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
@@ -1757,18 +1754,12 @@ packages:
'@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
- '@tsclass/tsclass@5.0.0':
- resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
-
'@tsclass/tsclass@9.3.0':
resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
- '@types/bn.js@5.2.0':
- resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
-
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -1787,12 +1778,6 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
- '@types/dns-packet@5.6.5':
- resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
-
- '@types/elliptic@6.4.18':
- resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
-
'@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
@@ -2096,9 +2081,6 @@ packages:
bintrees@1.0.2:
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
- bn.js@4.12.2:
- resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
-
body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
@@ -2119,9 +2101,6 @@ packages:
broadcast-channel@7.3.0:
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
- brorand@1.1.0:
- resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
-
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
@@ -2281,6 +2260,10 @@ packages:
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+ croner@10.0.1:
+ resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
+ engines: {node: '>=18.0'}
+
croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'}
@@ -2374,10 +2357,6 @@ packages:
devtools-protocol@0.0.1566079:
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
- dns-packet@5.6.1:
- resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
- engines: {node: '>=6'}
-
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -2407,9 +2386,6 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
- elliptic@6.6.1:
- resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
-
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2742,9 +2718,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
- hash.js@1.1.7:
- resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
-
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -2766,9 +2739,6 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
- hmac-drbg@1.0.1:
- resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
-
html-minifier@4.0.0:
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
engines: {node: '>=6'}
@@ -3279,12 +3249,6 @@ packages:
mingo@7.2.0:
resolution: {integrity: sha512-UeX942qZpofn5L97h295SkS7j/ADf7Qac8gdRCMBPxi0/1m70aeB2owLFvWbyuMj1dowonlivlVRQVDx+6h+7Q==}
- minimalistic-assert@1.0.1:
- resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
-
- minimalistic-crypto-utils@1.0.1:
- resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
-
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22}
@@ -4491,18 +4455,6 @@ snapshots:
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
- '@apiclient.xyz/cloudflare@6.4.3':
- dependencies:
- '@push.rocks/smartdelay': 3.0.5
- '@push.rocks/smartlog': 3.1.11
- '@push.rocks/smartpromise': 4.2.3
- '@push.rocks/smartrequest': 5.0.1
- '@push.rocks/smartstring': 4.1.0
- '@tsclass/tsclass': 9.3.0
- cloudflare: 5.2.0
- transitivePeerDependencies:
- - encoding
-
'@apiclient.xyz/cloudflare@7.1.0':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -5500,8 +5452,6 @@ snapshots:
'@isaacs/cliui@9.0.0': {}
- '@leichtgewicht/ip-codec@2.0.5': {}
-
'@lit-labs/ssr-dom-shim@1.5.1': {}
'@lit/reactive-element@2.1.2':
@@ -5832,24 +5782,21 @@ snapshots:
'@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0
- '@push.rocks/smartacme@8.0.0(socks@2.8.7)':
+ '@push.rocks/smartacme@9.1.2(socks@2.8.7)':
dependencies:
- '@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
- '@apiclient.xyz/cloudflare': 6.4.3
+ '@apiclient.xyz/cloudflare': 7.1.0
+ '@peculiar/x509': 1.14.3
'@push.rocks/lik': 6.2.2
- '@push.rocks/smartdata': 5.16.7(socks@2.8.7)
+ '@push.rocks/smartdata': 7.0.15(socks@2.8.7)
'@push.rocks/smartdelay': 3.0.5
- '@push.rocks/smartdns': 6.2.2
- '@push.rocks/smartfile': 11.2.7
+ '@push.rocks/smartdns': 7.8.1
'@push.rocks/smartlog': 3.1.11
'@push.rocks/smartnetwork': 4.4.0
- '@push.rocks/smartpromise': 4.2.3
- '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.1.0
- '@push.rocks/smarttime': 4.1.1
+ '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9
+ '@push.rocks/taskbuffer': 6.1.2
'@tsclass/tsclass': 9.3.0
- acme-client: 5.4.0
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
@@ -6036,22 +5983,6 @@ snapshots:
dependencies:
'@push.rocks/smartpromise': 4.2.3
- '@push.rocks/smartdns@6.2.2':
- dependencies:
- '@push.rocks/smartdelay': 3.0.5
- '@push.rocks/smartenv': 5.0.13
- '@push.rocks/smartpromise': 4.2.3
- '@push.rocks/smartrequest': 2.1.0
- '@tsclass/tsclass': 5.0.0
- '@types/dns-packet': 5.6.5
- '@types/elliptic': 6.4.18
- acme-client: 5.4.0
- dns-packet: 5.6.1
- elliptic: 6.6.1
- minimatch: 10.1.2
- transitivePeerDependencies:
- - supports-color
-
'@push.rocks/smartdns@7.8.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -6621,6 +6552,17 @@ snapshots:
is-nan: 1.3.2
pretty-ms: 9.3.0
+ '@push.rocks/smarttime@4.2.3':
+ dependencies:
+ '@push.rocks/lik': 6.2.2
+ '@push.rocks/smartdelay': 3.0.5
+ '@push.rocks/smartpromise': 4.2.3
+ croner: 10.0.1
+ date-fns: 4.1.0
+ dayjs: 1.11.19
+ is-nan: 1.3.2
+ pretty-ms: 9.3.0
+
'@push.rocks/smartunique@3.0.9':
dependencies:
'@types/uuid': 9.0.8
@@ -6688,6 +6630,22 @@ snapshots:
- supports-color
- vue
+ '@push.rocks/taskbuffer@6.1.2':
+ dependencies:
+ '@design.estate/dees-element': 2.1.6
+ '@push.rocks/lik': 6.2.2
+ '@push.rocks/smartdelay': 3.0.5
+ '@push.rocks/smartlog': 3.1.11
+ '@push.rocks/smartpromise': 4.2.3
+ '@push.rocks/smartrx': 3.0.10
+ '@push.rocks/smarttime': 4.2.3
+ '@push.rocks/smartunique': 3.0.9
+ transitivePeerDependencies:
+ - '@nuxt/kit'
+ - react
+ - supports-color
+ - vue
+
'@push.rocks/webrequest@3.0.37':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -7423,10 +7381,6 @@ snapshots:
dependencies:
type-fest: 4.41.0
- '@tsclass/tsclass@5.0.0':
- dependencies:
- type-fest: 4.41.0
-
'@tsclass/tsclass@9.3.0':
dependencies:
type-fest: 4.41.0
@@ -7436,10 +7390,6 @@ snapshots:
tslib: 2.8.1
optional: true
- '@types/bn.js@5.2.0':
- dependencies:
- '@types/node': 25.2.3
-
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@@ -7464,14 +7414,6 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
- '@types/dns-packet@5.6.5':
- dependencies:
- '@types/node': 25.2.3
-
- '@types/elliptic@6.4.18':
- dependencies:
- '@types/bn.js': 5.2.0
-
'@types/express-serve-static-core@5.1.1':
dependencies:
'@types/node': 25.2.3
@@ -7788,8 +7730,6 @@ snapshots:
bintrees@1.0.2: {}
- bn.js@4.12.2: {}
-
body-parser@2.2.2:
dependencies:
bytes: 3.1.2
@@ -7826,8 +7766,6 @@ snapshots:
p-queue: 6.6.2
unload: 2.4.1
- brorand@1.1.0: {}
-
bson@6.10.4: {}
bson@7.2.0: {}
@@ -7978,6 +7916,8 @@ snapshots:
crelt@1.0.6: {}
+ croner@10.0.1: {}
+
croner@9.1.0: {}
cross-spawn@7.0.6:
@@ -8050,10 +7990,6 @@ snapshots:
devtools-protocol@0.0.1566079: {}
- dns-packet@5.6.1:
- dependencies:
- '@leichtgewicht/ip-codec': 2.0.5
-
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -8090,16 +8026,6 @@ snapshots:
ee-first@1.1.1: {}
- elliptic@6.6.1:
- dependencies:
- bn.js: 4.12.2
- brorand: 1.1.0
- hash.js: 1.1.7
- hmac-drbg: 1.0.1
- inherits: 2.0.4
- minimalistic-assert: 1.0.1
- minimalistic-crypto-utils: 1.0.1
-
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -8529,11 +8455,6 @@ snapshots:
dependencies:
has-symbols: 1.1.0
- hash.js@1.1.7:
- dependencies:
- inherits: 2.0.4
- minimalistic-assert: 1.0.1
-
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -8566,12 +8487,6 @@ snapshots:
highlight.js@11.11.1: {}
- hmac-drbg@1.0.1:
- dependencies:
- hash.js: 1.1.7
- minimalistic-assert: 1.0.1
- minimalistic-crypto-utils: 1.0.1
-
html-minifier@4.0.0:
dependencies:
camel-case: 3.0.0
@@ -9280,10 +9195,6 @@ snapshots:
mingo@7.2.0: {}
- minimalistic-assert@1.0.1: {}
-
- minimalistic-crypto-utils@1.0.1: {}
-
minimatch@10.1.2:
dependencies:
'@isaacs/brace-expansion': 5.0.1
diff --git a/readme.md b/readme.md
index ca2eec4..2d2875e 100644
--- a/readme.md
+++ b/readme.md
@@ -21,6 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [Email System](#email-system)
- [DNS Server](#dns-server)
- [RADIUS Server](#radius-server)
+- [Certificate Management](#certificate-management)
- [Storage & Caching](#storage--caching)
- [Security Features](#security-features)
- [OpsServer Dashboard](#opsserver-dashboard)
@@ -46,7 +47,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Hierarchical rate limiting** β global, per-domain, per-sender
### π Enterprise Security
-- **Automatic TLS certificates** via ACME with Cloudflare DNS-01 challenges
+- **Automatic TLS certificates** via ACME (smartacme v9) with Cloudflare DNS-01 challenges
+- **Smart certificate scheduling** β per-domain deduplication, controlled parallelism, and account rate limiting handled automatically
+- **Per-domain exponential backoff** β failed provisioning attempts are tracked and backed off to avoid hammering ACME servers
- **IP reputation checking** with caching and configurable thresholds
- **Content scanning** for spam, viruses, and malicious attachments
- **Security event logging** with structured audit trails
@@ -73,7 +76,8 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### π₯οΈ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring
- **JWT authentication** with session persistence
-- **Live views** for connections, email queues, DNS queries, RADIUS sessions, and security events
+- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, and security events
+- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
- **Read-only configuration display** β DcRouter is configured through code
## Installation
@@ -250,7 +254,7 @@ graph TB
ES[smartmta Email Server
TypeScript + Rust]
DS[SmartDNS Server
Rust-powered]
RS[SmartRadius Server]
- CM[Certificate Manager]
+ CM[Certificate Manager
smartacme v9]
OS[OpsServer Dashboard]
MM[Metrics Manager]
SM[Storage Manager]
@@ -297,6 +301,7 @@ graph TB
| **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) |
| **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) |
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
+| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting |
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
@@ -308,8 +313,8 @@ graph TB
DcRouter acts purely as an **orchestrator** β it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, and SmartRadius based on which configs are provided.
-2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery.
-3. **On `stop()`**: All services are gracefully shut down in reverse order.
+2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
+3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
### Rust-Powered Architecture
@@ -584,15 +589,6 @@ match: { sizeRange: { min: 1000, max: 5000000 }, hasAttachments: true }
match: { subject: /invoice|receipt/i }
```
-### Socket-Handler Mode π
-
-When `useSocketHandler: true` is set, SmartProxy passes sockets directly to the email server β no internal port binding, lower latency, and fewer open ports:
-
-```
-Traditional: External Port β SmartProxy β Internal Port β Email Server
-Socket Mode: External Port β SmartProxy β (direct socket) β Email Server
-```
-
### Email Security Stack
- **DKIM** β Automatic key generation, signing, and rotation for all domains
@@ -705,6 +701,73 @@ RADIUS is fully manageable at runtime via the OpsServer API:
- Session monitoring and forced disconnects
- Accounting summaries and statistics
+## Certificate Management
+
+DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
+
+### How It Works
+
+When a `dnsChallenge` is configured (e.g. with a Cloudflare API key), DcRouter creates a SmartAcme instance that handles DNS-01 challenges for automatic certificate provisioning. SmartProxy calls the `certProvisionFunction` whenever a route needs a TLS certificate, and SmartAcme takes care of the rest.
+
+```typescript
+const router = new DcRouter({
+ smartProxyConfig: {
+ routes: [
+ {
+ name: 'secure-app',
+ match: { domains: ['app.example.com'], ports: [443] },
+ action: {
+ type: 'forward',
+ targets: [{ host: '192.168.1.10', port: 8080 }],
+ tls: { mode: 'terminate', certificate: 'auto' } // β triggers ACME provisioning
+ }
+ }
+ ],
+ acme: { email: 'admin@example.com', enabled: true, useProduction: true }
+ },
+ tls: { contactEmail: 'admin@example.com' },
+ dnsChallenge: { cloudflareApiKey: process.env.CLOUDFLARE_API_KEY }
+});
+```
+
+### smartacme v9 Features
+
+| Feature | Description |
+|---------|-------------|
+| **Per-domain deduplication** | Concurrent requests for the same domain share a single ACME operation |
+| **Global concurrency cap** | Default 5 parallel ACME operations to prevent overload |
+| **Account rate limiting** | Sliding window (250 orders / 3 hours) to stay within ACME provider limits |
+| **Structured errors** | `AcmeError` with `isRetryable`, `isRateLimited`, `retryAfter` fields |
+| **Clean shutdown** | `stop()` properly destroys HTTP agents and DNS clients |
+
+### Per-Domain Backoff
+
+DcRouter's `CertProvisionScheduler` adds **per-domain exponential backoff** on top of smartacme's built-in protections. If a DNS-01 challenge fails for a domain:
+
+1. The failure is recorded (persisted to storage)
+2. The domain enters backoff: `min(failuresΒ² Γ 1 hour, 24 hours)`
+3. Subsequent requests for that domain are rejected until the backoff expires
+4. On success, the backoff is cleared
+
+This prevents hammering ACME servers for domains with persistent issues (e.g. missing DNS delegation).
+
+### Fallback to HTTP-01
+
+If DNS-01 fails, the `certProvisionFunction` returns `'http01'` to tell SmartProxy to fall back to HTTP-01 challenge validation. This provides a safety net for domains where DNS-01 isn't viable.
+
+### Certificate Storage
+
+Certificates are persisted via the `StorageBackedCertManager` which uses DcRouter's `StorageManager`. This means certs survive restarts and don't need to be re-provisioned unless they expire.
+
+### Dashboard
+
+The OpsServer includes a **Certificates** view showing:
+- All domains with their certificate status (valid, expiring, expired, failed)
+- Certificate source (ACME, provision function, static)
+- Expiry dates and issuer information
+- Backoff status for failed domains
+- One-click reprovisioning per domain
+
## Storage & Caching
### StorageManager
@@ -725,7 +788,7 @@ storage: {
// Simply omit the storage config
```
-Used for: DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs.
+Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state.
### Cache Database
@@ -811,6 +874,7 @@ The OpsServer provides a web-based management interface served on port 3000. It'
| π **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
| π **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
| π§ **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
+| π **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning |
| π **Logs** | Real-time log viewer with level filtering and search |
| βοΈ **Configuration** | Read-only view of current system configuration |
| π‘οΈ **Security** | IP reputation, rate limit status, blocked connections |
@@ -838,6 +902,11 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'getBounceRecords' // Bounce records
'removeFromSuppressionList' // Unsuppress an address
+// Certificates
+'getCertificateOverview' // Domain-centric certificate status
+'reprovisionCertificate' // Reprovision by route name (legacy)
+'reprovisionCertificateDomain' // Reprovision by domain (preferred)
+
// Configuration (read-only)
'getConfiguration' // Current system config
@@ -884,6 +953,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|----------|------|-------------|
| `options` | `IDcRouterOptions` | Current configuration |
| `smartProxy` | `SmartProxy` | SmartProxy instance |
+| `smartAcme` | `SmartAcme` | SmartAcme v9 certificate manager instance |
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
| `dnsServer` | `DnsServer` | DNS server instance |
| `radiusServer` | `RadiusServer` | RADIUS server instance |
@@ -891,6 +961,8 @@ const router = new DcRouter(options: IDcRouterOptions);
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance |
+| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
+| `certificateStatusMap` | `Map` | Domain-keyed certificate status from SmartProxy events |
### Re-exported Types
diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts
index 6a8e1c2..953eb63 100644
--- a/ts/00_commitinfo_data.ts
+++ b/ts/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
- version: '6.0.0',
+ version: '6.1.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
diff --git a/ts/classes.cert-provision-scheduler.ts b/ts/classes.cert-provision-scheduler.ts
index 035c3c7..e70bdc6 100644
--- a/ts/classes.cert-provision-scheduler.ts
+++ b/ts/classes.cert-provision-scheduler.ts
@@ -11,31 +11,22 @@ interface IBackoffEntry {
/**
* Manages certificate provisioning scheduling with:
* - Per-domain exponential backoff persisted in StorageManager
- * - Serial stagger queue with configurable delay between provisions
+ *
+ * Note: Serial stagger queue was removed β smartacme v9 handles
+ * concurrency, per-domain dedup, and rate limiting internally.
*/
export class CertProvisionScheduler {
private storageManager: StorageManager;
- private staggerDelayMs: number;
private maxBackoffHours: number;
- // In-memory serial queue
- private queue: Array<{
- domain: string;
- fn: () => Promise;
- resolve: (value: any) => void;
- reject: (err: any) => void;
- }> = [];
- private processing = false;
-
// In-memory backoff cache (mirrors storage for fast lookups)
private backoffCache = new Map();
constructor(
storageManager: StorageManager,
- options?: { staggerDelayMs?: number; maxBackoffHours?: number }
+ options?: { maxBackoffHours?: number }
) {
this.storageManager = storageManager;
- this.staggerDelayMs = options?.staggerDelayMs ?? 3000;
this.maxBackoffHours = options?.maxBackoffHours ?? 24;
}
@@ -136,41 +127,4 @@ export class CertProvisionScheduler {
lastError: entry.lastError,
};
}
-
- /**
- * Enqueue a provision operation for serial execution with stagger delay.
- * Returns the result of the provision function.
- */
- enqueueProvision(domain: string, fn: () => Promise): Promise {
- return new Promise((resolve, reject) => {
- this.queue.push({ domain, fn, resolve, reject });
- this.processQueue();
- });
- }
-
- /**
- * Process the stagger queue serially
- */
- private async processQueue(): Promise {
- if (this.processing) return;
- this.processing = true;
-
- while (this.queue.length > 0) {
- const item = this.queue.shift()!;
- try {
- logger.log('info', `Processing cert provision for ${item.domain}`);
- const result = await item.fn();
- item.resolve(result);
- } catch (err) {
- item.reject(err);
- }
-
- // Stagger delay between provisions
- if (this.queue.length > 0) {
- await new Promise((r) => setTimeout(r, this.staggerDelayMs));
- }
- }
-
- this.processing = false;
- }
}
diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts
index 4f2d3ce..787193a 100644
--- a/ts/classes.dcrouter.ts
+++ b/ts/classes.dcrouter.ts
@@ -195,7 +195,7 @@ export class DcRouter {
error?: string;
}>();
- // Certificate provisioning scheduler with backoff + stagger
+ // Certificate provisioning scheduler with per-domain backoff
public certProvisionScheduler?: CertProvisionScheduler;
// TypedRouter for API endpoints
@@ -496,23 +496,22 @@ export class DcRouter {
}
try {
- const result = await scheduler.enqueueProvision(domain, async () => {
- eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
- eventComms.setSource('smartacme-dns-01');
- const cert = await this.smartAcme.getCertificateForDomain(domain);
- if (cert.validUntil) {
- eventComms.setExpiryDate(new Date(cert.validUntil));
- }
- return {
- id: cert.id,
- domainName: cert.domainName,
- created: cert.created,
- validUntil: cert.validUntil,
- privateKey: cert.privateKey,
- publicKey: cert.publicKey,
- csr: cert.csr,
- };
- });
+ // smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
+ eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
+ eventComms.setSource('smartacme-dns-01');
+ const cert = await this.smartAcme.getCertificateForDomain(domain);
+ if (cert.validUntil) {
+ eventComms.setExpiryDate(new Date(cert.validUntil));
+ }
+ const result = {
+ id: cert.id,
+ domainName: cert.domainName,
+ created: cert.created,
+ validUntil: cert.validUntil,
+ privateKey: cert.privateKey,
+ publicKey: cert.publicKey,
+ csr: cert.csr,
+ };
// Success β clear any backoff
await scheduler.clearBackoff(domain);
diff --git a/ts_interfaces/readme.md b/ts_interfaces/readme.md
index 67a6e38..b9b36c9 100644
--- a/ts_interfaces/readme.md
+++ b/ts_interfaces/readme.md
@@ -128,6 +128,37 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
+#### π Certificates
+| Interface | Method | Description |
+|-----------|--------|-------------|
+| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status |
+| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) |
+| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) |
+
+#### Certificate Types
+```typescript
+type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown';
+type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
+
+interface ICertificateInfo {
+ domain: string;
+ routeNames: string[];
+ status: TCertificateStatus;
+ source: TCertificateSource;
+ tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
+ expiryDate?: string;
+ issuer?: string;
+ issuedAt?: string;
+ error?: string;
+ canReprovision: boolean;
+ backoffInfo?: {
+ failures: number;
+ retryAfter?: string;
+ lastError?: string;
+ };
+}
+```
+
#### π‘ RADIUS
| Interface | Method | Description |
|-----------|--------|-------------|
@@ -173,7 +204,16 @@ console.log('Email:', metrics.emailStats);
console.log('DNS:', metrics.dnsStats);
console.log('Security:', metrics.securityMetrics);
-// 3. Check email queues
+// 3. Check certificate status
+const certClient = new typedrequest.TypedRequest(
+ 'https://your-dcrouter:3000/typedrequest',
+ 'getCertificateOverview'
+);
+
+const certs = await certClient.fire({ identity });
+console.log(`Certificates: ${certs.summary.valid} valid, ${certs.summary.failed} failed`);
+
+// 4. Check email queues
const queueClient = new typedrequest.TypedRequest(
'https://your-dcrouter:3000/typedrequest',
'getQueuedEmails'
diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts
index 6a8e1c2..953eb63 100644
--- a/ts_web/00_commitinfo_data.ts
+++ b/ts_web/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
- version: '6.0.0',
+ version: '6.1.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}