From 0a6315f177590817b403cba369648e4a22220050 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 16 Feb 2026 00:22:23 +0000 Subject: [PATCH] feat(certs): integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler --- changelog.md | 11 ++ package.json | 2 +- pnpm-lock.yaml | 189 +++++++------------------ readme.md | 102 +++++++++++-- ts/00_commitinfo_data.ts | 2 +- ts/classes.cert-provision-scheduler.ts | 54 +------ ts/classes.dcrouter.ts | 35 +++-- ts_interfaces/readme.md | 42 +++++- ts_web/00_commitinfo_data.ts | 2 +- 9 files changed, 213 insertions(+), 226 deletions(-) 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.' }