Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 841f99e19d | |||
| 8e9de46cd2 | |||
| 2d44528345 | |||
| 28a38252da | |||
| dfb268bbfc | |||
| 6532c7ff22 |
27
changelog.md
27
changelog.md
@@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
- Add CertProvisionScheduler: persistent per-domain exponential backoff, retry calculation, and an in-memory serial stagger queue.
|
||||||
|
- Integrate scheduler with SmartAcme certProvisionFunction: enqueue provisions, clear backoff on success, record failures to drive backoff.
|
||||||
|
- Switch certificate event tracking to be keyed by domain (certificateStatusMap now keyed by domain) and add findRouteNamesForDomain helper.
|
||||||
|
- BREAKING: ICertificateInfo shape changed — replaced routeName/domains with domain and routeNames; added optional backoffInfo (failures, retryAfter, lastError).
|
||||||
|
- Add domain-based reprovision endpoint (reprovisionCertificateDomain) while retaining legacy route-based reprovision for backward compatibility (internal rename to reprovisionCertificateByRoute).
|
||||||
|
- Web UI updated to domain-centric certificate overview, displays route pills, backoff indicator and retry timing, and uses domain-based reprovision action.
|
||||||
|
- Dependency bumps: @push.rocks/smartlog -> ^3.1.11, @push.rocks/smartproxy -> ^25.3.1.
|
||||||
|
|
||||||
|
## 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": "6.0.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": {
|
||||||
@@ -42,14 +42,14 @@
|
|||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.1.11",
|
||||||
"@push.rocks/smartmetrics": "^2.0.10",
|
"@push.rocks/smartmetrics": "^2.0.10",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartmta": "^5.2.2",
|
"@push.rocks/smartmta": "^5.2.2",
|
||||||
"@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.1",
|
||||||
"@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",
|
||||||
|
|||||||
129
pnpm-lock.yaml
generated
129
pnpm-lock.yaml
generated
@@ -37,7 +37,7 @@ importers:
|
|||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
'@push.rocks/smartacme':
|
'@push.rocks/smartacme':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 8.0.0(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)
|
||||||
@@ -54,8 +54,8 @@ importers:
|
|||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
'@push.rocks/smartlog':
|
'@push.rocks/smartlog':
|
||||||
specifier: ^3.1.10
|
specifier: ^3.1.11
|
||||||
version: 3.1.10
|
version: 3.1.11
|
||||||
'@push.rocks/smartmetrics':
|
'@push.rocks/smartmetrics':
|
||||||
specifier: ^2.0.10
|
specifier: ^2.0.10
|
||||||
version: 2.0.10
|
version: 2.0.10
|
||||||
@@ -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.1
|
||||||
version: 25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 25.3.1
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -116,7 +116,7 @@ importers:
|
|||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
version: 3.1.0(@tiptap/pm@2.27.2)
|
||||||
@@ -970,8 +970,8 @@ packages:
|
|||||||
'@push.rocks/smartlog-interfaces@3.0.2':
|
'@push.rocks/smartlog-interfaces@3.0.2':
|
||||||
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.1.10':
|
'@push.rocks/smartlog@3.1.11':
|
||||||
resolution: {integrity: sha512-5pf5JyzOE2WTCUislNIW4EHePo1a7hiXB+jbil38+N5hW71AEwcPFe6oGxbp5w9ALlz66hV2+E+25R0SsxN+fQ==}
|
resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==}
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
||||||
@@ -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.1':
|
||||||
resolution: {integrity: sha512-cwqtfSI3QziyZOYXZuL4/jq1KHXQRVwGvimHcqhJDsl4cac9y7fM4gKHU4B3m2/2qaih1scP9FPGwlCCVFXR7Q==}
|
resolution: {integrity: sha512-kGJGpx3KBUz+qWU2L9B2gbZoUbQEG2BFe6ZzK0b68Y32nHoSIMjol14hzc3sRgW1p/loWy+Gj+5j0KuVytKWmA==}
|
||||||
|
|
||||||
'@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==}
|
||||||
@@ -1131,6 +1131,9 @@ packages:
|
|||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
||||||
|
|
||||||
|
'@push.rocks/webrequest@4.0.1':
|
||||||
|
resolution: {integrity: sha512-I60XZZLVf8W5I7YdmUVVu4G92teE3rg3/aKaV00BRg8vJ3VXx3wc59Qj4em7zxQ5o0HvL8m1Aezw3RFMDPyVgA==}
|
||||||
|
|
||||||
'@push.rocks/webrequest@4.0.2':
|
'@push.rocks/webrequest@4.0.2':
|
||||||
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
|
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
|
||||||
|
|
||||||
@@ -1847,10 +1850,6 @@ packages:
|
|||||||
'@types/minimatch@5.1.2':
|
'@types/minimatch@5.1.2':
|
||||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
|
||||||
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
|
||||||
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
|
||||||
|
|
||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
@@ -4379,7 +4378,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfeed': 1.4.0
|
'@push.rocks/smartfeed': 1.4.0
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@push.rocks/smartmanifest': 2.0.2
|
'@push.rocks/smartmanifest': 2.0.2
|
||||||
@@ -4428,7 +4427,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@push.rocks/smartmanifest': 2.0.2
|
'@push.rocks/smartmanifest': 2.0.2
|
||||||
@@ -4495,7 +4494,7 @@ snapshots:
|
|||||||
'@apiclient.xyz/cloudflare@6.4.3':
|
'@apiclient.xyz/cloudflare@6.4.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
@@ -4507,7 +4506,7 @@ snapshots:
|
|||||||
'@apiclient.xyz/cloudflare@7.1.0':
|
'@apiclient.xyz/cloudflare@7.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
@@ -5241,7 +5240,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -5262,7 +5261,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -5288,7 +5287,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartnpm': 2.0.6
|
'@push.rocks/smartnpm': 2.0.6
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
@@ -5308,7 +5307,7 @@ snapshots:
|
|||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
|
|
||||||
'@git.zone/tstest@3.1.8(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.8.3
|
||||||
@@ -5323,7 +5322,7 @@ snapshots:
|
|||||||
'@push.rocks/smartexpect': 2.5.0
|
'@push.rocks/smartexpect': 2.5.0
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.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
|
||||||
@@ -5339,6 +5338,7 @@ snapshots:
|
|||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
|
- '@push.rocks/smartserve'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
@@ -5368,7 +5368,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
@@ -5805,7 +5805,7 @@ snapshots:
|
|||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@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/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -5829,10 +5829,10 @@ snapshots:
|
|||||||
'@api.global/typedrequest': 3.2.6
|
'@api.global/typedrequest': 3.2.6
|
||||||
'@configvault.io/interfaces': 1.0.17
|
'@configvault.io/interfaces': 1.0.17
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartacme@8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
'@push.rocks/smartacme@8.0.0(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@apiclient.xyz/cloudflare': 6.4.3
|
'@apiclient.xyz/cloudflare': 6.4.3
|
||||||
@@ -5841,7 +5841,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartdns': 6.2.2
|
'@push.rocks/smartdns': 6.2.2
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
@@ -5854,9 +5854,7 @@ snapshots:
|
|||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- '@push.rocks/smartserve'
|
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
- bufferutil
|
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
@@ -5866,7 +5864,6 @@ snapshots:
|
|||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartarchive@4.2.4':
|
'@push.rocks/smartarchive@4.2.4':
|
||||||
@@ -5956,7 +5953,7 @@ snapshots:
|
|||||||
'@push.rocks/smartcli@4.0.20':
|
'@push.rocks/smartcli@4.0.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartobject': 1.0.12
|
'@push.rocks/smartobject': 1.0.12
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -5981,7 +5978,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -6010,7 +6007,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -6218,7 +6215,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 2.0.2
|
'@api.global/typedrequest-interfaces': 2.0.2
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.1.10':
|
'@push.rocks/smartlog@3.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -6228,7 +6225,7 @@ snapshots:
|
|||||||
'@push.rocks/smarthash': 3.2.6
|
'@push.rocks/smarthash': 3.2.6
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 4.0.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
@@ -6265,7 +6262,7 @@ snapshots:
|
|||||||
'@push.rocks/smartmetrics@2.0.10':
|
'@push.rocks/smartmetrics@2.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@types/pidusage': 2.0.5
|
'@types/pidusage': 2.0.5
|
||||||
pidtree: 0.6.0
|
pidtree: 0.6.0
|
||||||
pidusage: 4.0.1
|
pidusage: 4.0.1
|
||||||
@@ -6338,7 +6335,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmail': 2.2.0
|
'@push.rocks/smartmail': 2.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
@@ -6441,45 +6438,13 @@ 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.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
|
||||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartfile': 13.1.2
|
|
||||||
'@push.rocks/smartlog': 3.1.10
|
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
'@push.rocks/smartrx': 3.0.10
|
|
||||||
'@push.rocks/smartstring': 4.1.0
|
|
||||||
'@push.rocks/taskbuffer': 4.2.0
|
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
'@types/minimatch': 6.0.0
|
|
||||||
'@types/ws': 8.18.1
|
|
||||||
minimatch: 10.2.0
|
minimatch: 10.2.0
|
||||||
pretty-ms: 9.3.0
|
|
||||||
ws: 8.19.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@aws-sdk/credential-providers'
|
|
||||||
- '@mongodb-js/zstd'
|
|
||||||
- '@nuxt/kit'
|
|
||||||
- '@push.rocks/smartserve'
|
|
||||||
- bare-abort-controller
|
|
||||||
- bufferutil
|
|
||||||
- encoding
|
|
||||||
- gcp-metadata
|
|
||||||
- kerberos
|
|
||||||
- mongodb-client-encryption
|
|
||||||
- react
|
|
||||||
- react-native-b4a
|
|
||||||
- snappy
|
|
||||||
- socks
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6557,7 +6522,7 @@ snapshots:
|
|||||||
'@cfworker/json-schema': 4.1.1
|
'@cfworker/json-schema': 4.1.1
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartenv': 6.0.0
|
'@push.rocks/smartenv': 6.0.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6592,7 +6557,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6696,7 +6661,7 @@ snapshots:
|
|||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6712,7 +6677,7 @@ snapshots:
|
|||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6731,6 +6696,14 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
|
'@push.rocks/webrequest@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartenv': 5.0.13
|
||||||
|
'@push.rocks/smartjson': 5.2.0
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
'@push.rocks/webrequest@4.0.2':
|
'@push.rocks/webrequest@4.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -7570,10 +7543,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/minimatch@5.1.2': {}
|
'@types/minimatch@5.1.2': {}
|
||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
|
||||||
dependencies:
|
|
||||||
minimatch: 10.2.0
|
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/mute-stream@0.0.4':
|
'@types/mute-stream@0.0.4':
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.5',
|
version: '6.0.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
176
ts/classes.cert-provision-scheduler.ts
Normal file
176
ts/classes.cert-provision-scheduler.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { logger } from './logger.js';
|
||||||
|
import type { StorageManager } from './storage/index.js';
|
||||||
|
|
||||||
|
interface IBackoffEntry {
|
||||||
|
failures: number;
|
||||||
|
lastFailure: string; // ISO string
|
||||||
|
retryAfter: string; // ISO string
|
||||||
|
lastError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages certificate provisioning scheduling with:
|
||||||
|
* - Per-domain exponential backoff persisted in StorageManager
|
||||||
|
* - Serial stagger queue with configurable delay between provisions
|
||||||
|
*/
|
||||||
|
export class CertProvisionScheduler {
|
||||||
|
private storageManager: StorageManager;
|
||||||
|
private staggerDelayMs: number;
|
||||||
|
private maxBackoffHours: number;
|
||||||
|
|
||||||
|
// In-memory serial queue
|
||||||
|
private queue: Array<{
|
||||||
|
domain: string;
|
||||||
|
fn: () => Promise<any>;
|
||||||
|
resolve: (value: any) => void;
|
||||||
|
reject: (err: any) => void;
|
||||||
|
}> = [];
|
||||||
|
private processing = false;
|
||||||
|
|
||||||
|
// In-memory backoff cache (mirrors storage for fast lookups)
|
||||||
|
private backoffCache = new Map<string, IBackoffEntry>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
storageManager: StorageManager,
|
||||||
|
options?: { staggerDelayMs?: number; maxBackoffHours?: number }
|
||||||
|
) {
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
this.staggerDelayMs = options?.staggerDelayMs ?? 3000;
|
||||||
|
this.maxBackoffHours = options?.maxBackoffHours ?? 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage key for a domain's backoff entry
|
||||||
|
*/
|
||||||
|
private backoffKey(domain: string): string {
|
||||||
|
const clean = domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
return `/cert-backoff/${clean}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load backoff entry from storage (with in-memory cache)
|
||||||
|
*/
|
||||||
|
private async loadBackoff(domain: string): Promise<IBackoffEntry | null> {
|
||||||
|
const cached = this.backoffCache.get(domain);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const entry = await this.storageManager.getJSON<IBackoffEntry>(this.backoffKey(domain));
|
||||||
|
if (entry) {
|
||||||
|
this.backoffCache.set(domain, entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save backoff entry to both cache and storage
|
||||||
|
*/
|
||||||
|
private async saveBackoff(domain: string, entry: IBackoffEntry): Promise<void> {
|
||||||
|
this.backoffCache.set(domain, entry);
|
||||||
|
await this.storageManager.setJSON(this.backoffKey(domain), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a domain is currently in backoff
|
||||||
|
*/
|
||||||
|
async isInBackoff(domain: string): Promise<boolean> {
|
||||||
|
const entry = await this.loadBackoff(domain);
|
||||||
|
if (!entry) return false;
|
||||||
|
|
||||||
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
|
return retryAfter.getTime() > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a provisioning failure for a domain.
|
||||||
|
* Sets exponential backoff: min(failures^2 * 1h, maxBackoffHours)
|
||||||
|
*/
|
||||||
|
async recordFailure(domain: string, error?: string): Promise<void> {
|
||||||
|
const existing = await this.loadBackoff(domain);
|
||||||
|
const failures = (existing?.failures ?? 0) + 1;
|
||||||
|
|
||||||
|
// Exponential backoff: failures^2 hours, capped
|
||||||
|
const backoffHours = Math.min(failures * failures, this.maxBackoffHours);
|
||||||
|
const retryAfter = new Date(Date.now() + backoffHours * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const entry: IBackoffEntry = {
|
||||||
|
failures,
|
||||||
|
lastFailure: new Date().toISOString(),
|
||||||
|
retryAfter: retryAfter.toISOString(),
|
||||||
|
lastError: error,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveBackoff(domain, entry);
|
||||||
|
logger.log('warn', `Cert backoff for ${domain}: ${failures} failures, retry after ${retryAfter.toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear backoff for a domain (on success or manual override)
|
||||||
|
*/
|
||||||
|
async clearBackoff(domain: string): Promise<void> {
|
||||||
|
this.backoffCache.delete(domain);
|
||||||
|
try {
|
||||||
|
await this.storageManager.delete(this.backoffKey(domain));
|
||||||
|
} catch {
|
||||||
|
// Ignore delete errors (key may not exist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backoff info for UI display
|
||||||
|
*/
|
||||||
|
async getBackoffInfo(domain: string): Promise<{
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string;
|
||||||
|
lastError?: string;
|
||||||
|
} | null> {
|
||||||
|
const entry = await this.loadBackoff(domain);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
// Only return if still in backoff
|
||||||
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
|
if (retryAfter.getTime() <= Date.now()) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
failures: entry.failures,
|
||||||
|
retryAfter: entry.retryAfter,
|
||||||
|
lastError: entry.lastError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a provision operation for serial execution with stagger delay.
|
||||||
|
* Returns the result of the provision function.
|
||||||
|
*/
|
||||||
|
enqueueProvision<T>(domain: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
this.queue.push({ domain, fn, resolve, reject });
|
||||||
|
this.processQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the stagger queue serially
|
||||||
|
*/
|
||||||
|
private async processQueue(): Promise<void> {
|
||||||
|
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<void>((r) => setTimeout(r, this.staggerDelayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ 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 { CertProvisionScheduler } from './classes.cert-provision-scheduler.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';
|
||||||
|
|
||||||
@@ -183,16 +185,19 @@ export class DcRouter {
|
|||||||
public cacheDb?: CacheDb;
|
public cacheDb?: CacheDb;
|
||||||
public cacheCleaner?: CacheCleaner;
|
public cacheCleaner?: CacheCleaner;
|
||||||
|
|
||||||
// Certificate status tracking from SmartProxy events
|
// Certificate status tracking from SmartProxy events (keyed by domain)
|
||||||
public certificateStatusMap = new Map<string, {
|
public certificateStatusMap = new Map<string, {
|
||||||
status: 'valid' | 'failed';
|
status: 'valid' | 'failed';
|
||||||
domain: string;
|
routeNames: string[];
|
||||||
expiryDate?: string;
|
expiryDate?: string;
|
||||||
issuedAt?: string;
|
issuedAt?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// Certificate provisioning scheduler with backoff + stagger
|
||||||
|
public certProvisionScheduler?: CertProvisionScheduler;
|
||||||
|
|
||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
@@ -205,6 +210,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,22 +449,54 @@ 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}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize cert provision scheduler
|
||||||
|
this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager);
|
||||||
|
|
||||||
// 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'],
|
||||||
});
|
});
|
||||||
await this.smartAcme.start();
|
await this.smartAcme.start();
|
||||||
|
|
||||||
|
const scheduler = this.certProvisionScheduler;
|
||||||
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
||||||
|
// Check backoff before attempting provision
|
||||||
|
if (await scheduler.isInBackoff(domain)) {
|
||||||
|
const info = await scheduler.getBackoffInfo(domain);
|
||||||
|
const msg = `Domain ${domain} is in backoff (${info?.failures} failures), retry after ${info?.retryAfter}`;
|
||||||
|
eventComms.warn(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const result = await scheduler.enqueueProvision(domain, async () => {
|
||||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||||
eventComms.setSource('smartacme-dns-01');
|
eventComms.setSource('smartacme-dns-01');
|
||||||
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
||||||
@@ -468,7 +512,14 @@ export class DcRouter {
|
|||||||
publicKey: cert.publicKey,
|
publicKey: cert.publicKey,
|
||||||
csr: cert.csr,
|
csr: cert.csr,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Success — clear any backoff
|
||||||
|
await scheduler.clearBackoff(domain);
|
||||||
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Record failure for backoff tracking
|
||||||
|
await scheduler.recordFailure(domain, err.message);
|
||||||
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
||||||
return 'http01';
|
return 'http01';
|
||||||
}
|
}
|
||||||
@@ -492,39 +543,34 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||||
|
// Events are keyed by domain for domain-centric certificate tracking
|
||||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'valid', routeNames,
|
||||||
status: 'valid', domain: event.domain,
|
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'valid', routeNames,
|
||||||
status: 'valid', domain: event.domain,
|
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'failed', routeNames, error: event.error,
|
||||||
status: 'failed', domain: event.domain, error: event.error,
|
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start SmartProxy
|
// Start SmartProxy
|
||||||
@@ -697,7 +743,7 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the route name that matches a given domain
|
* Find the first route name that matches a given domain
|
||||||
*/
|
*/
|
||||||
private findRouteNameForDomain(domain: string): string | undefined {
|
private findRouteNameForDomain(domain: string): string | undefined {
|
||||||
if (!this.smartProxy) return undefined;
|
if (!this.smartProxy) return undefined;
|
||||||
@@ -713,6 +759,27 @@ export class DcRouter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find ALL route names that match a given domain
|
||||||
|
*/
|
||||||
|
public findRouteNamesForDomain(domain: string): string[] {
|
||||||
|
if (!this.smartProxy) return [];
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const route of this.smartProxy.routeManager.getRoutes()) {
|
||||||
|
if (!route.match.domains || !route.name) continue;
|
||||||
|
const routeDomains = Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains
|
||||||
|
: [route.match.domains];
|
||||||
|
for (const pattern of routeDomains) {
|
||||||
|
if (this.isDomainMatch(domain, pattern)) {
|
||||||
|
names.push(route.name);
|
||||||
|
break; // This route already matched, no need to check other patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
console.log('Stopping DcRouter services...');
|
console.log('Stopping DcRouter services...');
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,24 +23,45 @@ export class CertificateHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reprovision Certificate
|
// Legacy route-based reprovision (backward compat)
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||||
'reprovisionCertificate',
|
'reprovisionCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
return this.reprovisionCertificate(dataArg.routeName);
|
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Domain-based reprovision (preferred)
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||||
|
'reprovisionCertificateDomain',
|
||||||
|
async (dataArg) => {
|
||||||
|
return this.reprovisionCertificateDomain(dataArg.domain);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build domain-centric certificate overview.
|
||||||
|
* Instead of one row per route, we produce one row per unique domain.
|
||||||
|
*/
|
||||||
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
const smartProxy = dcRouter.smartProxy;
|
const smartProxy = dcRouter.smartProxy;
|
||||||
if (!smartProxy) return [];
|
if (!smartProxy) return [];
|
||||||
|
|
||||||
const routes = smartProxy.routeManager.getRoutes();
|
const routes = smartProxy.routeManager.getRoutes();
|
||||||
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
|
||||||
|
// Phase 1: Collect unique domains with their associated route info
|
||||||
|
const domainMap = new Map<string, {
|
||||||
|
routeNames: string[];
|
||||||
|
source: interfaces.requests.TCertificateSource;
|
||||||
|
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
canReprovision: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (!route.name) continue;
|
if (!route.name) continue;
|
||||||
@@ -58,7 +79,6 @@ export class CertificateHandler {
|
|||||||
// Determine source
|
// Determine source
|
||||||
let source: interfaces.requests.TCertificateSource = 'none';
|
let source: interfaces.requests.TCertificateSource = 'none';
|
||||||
if (tls.certificate === 'auto') {
|
if (tls.certificate === 'auto') {
|
||||||
// Check if a certProvisionFunction is configured
|
|
||||||
if ((smartProxy.settings as any).certProvisionFunction) {
|
if ((smartProxy.settings as any).certProvisionFunction) {
|
||||||
source = 'provision-function';
|
source = 'provision-function';
|
||||||
} else {
|
} else {
|
||||||
@@ -68,15 +88,44 @@ export class CertificateHandler {
|
|||||||
source = 'static';
|
source = 'static';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with unknown status
|
const canReprovision = source === 'acme' || source === 'provision-function';
|
||||||
|
const tlsMode = tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
|
||||||
|
for (const domain of routeDomains) {
|
||||||
|
const existing = domainMap.get(domain);
|
||||||
|
if (existing) {
|
||||||
|
// Add this route name to the existing domain entry
|
||||||
|
if (!existing.routeNames.includes(route.name)) {
|
||||||
|
existing.routeNames.push(route.name);
|
||||||
|
}
|
||||||
|
// Upgrade source if more specific
|
||||||
|
if (existing.source === 'none' && source !== 'none') {
|
||||||
|
existing.source = source;
|
||||||
|
existing.canReprovision = canReprovision;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
domainMap.set(domain, {
|
||||||
|
routeNames: [route.name],
|
||||||
|
source,
|
||||||
|
tlsMode,
|
||||||
|
canReprovision,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Resolve status for each unique domain
|
||||||
|
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
||||||
|
|
||||||
|
for (const [domain, info] of domainMap) {
|
||||||
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
||||||
let expiryDate: string | undefined;
|
let expiryDate: string | undefined;
|
||||||
let issuedAt: string | undefined;
|
let issuedAt: string | undefined;
|
||||||
let issuer: string | undefined;
|
let issuer: string | undefined;
|
||||||
let error: string | undefined;
|
let error: string | undefined;
|
||||||
|
|
||||||
// Check event-based status from DcRouter's certificateStatusMap
|
// Check event-based status from certificateStatusMap (now keyed by domain)
|
||||||
const eventStatus = dcRouter.certificateStatusMap.get(route.name);
|
const eventStatus = dcRouter.certificateStatusMap.get(domain);
|
||||||
if (eventStatus) {
|
if (eventStatus) {
|
||||||
status = eventStatus.status;
|
status = eventStatus.status;
|
||||||
expiryDate = eventStatus.expiryDate;
|
expiryDate = eventStatus.expiryDate;
|
||||||
@@ -87,10 +136,10 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Rust-side certificate status if no event data
|
// Try SmartProxy certificate status if no event data
|
||||||
if (status === 'unknown') {
|
if (status === 'unknown' && info.routeNames.length > 0) {
|
||||||
try {
|
try {
|
||||||
const rustStatus = await smartProxy.getCertificateStatus(route.name);
|
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
|
||||||
if (rustStatus) {
|
if (rustStatus) {
|
||||||
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
||||||
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
||||||
@@ -104,7 +153,20 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
// Check persisted cert data from StorageManager
|
||||||
|
if (status === 'unknown') {
|
||||||
|
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 (expiryDate && (status === 'valid' || status === 'unknown')) {
|
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
||||||
const expiry = new Date(expiryDate);
|
const expiry = new Date(expiryDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -120,23 +182,36 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Static certs with no other info default to 'valid'
|
// Static certs with no other info default to 'valid'
|
||||||
if (source === 'static' && status === 'unknown') {
|
if (info.source === 'static' && status === 'unknown') {
|
||||||
status = 'valid';
|
status = 'valid';
|
||||||
}
|
}
|
||||||
|
|
||||||
const canReprovision = source === 'acme' || source === 'provision-function';
|
// ACME/provision-function routes with no cert data are still provisioning
|
||||||
|
if (status === 'unknown' && (info.source === 'acme' || info.source === 'provision-function')) {
|
||||||
|
status = 'provisioning';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Attach backoff info
|
||||||
|
let backoffInfo: interfaces.requests.ICertificateInfo['backoffInfo'];
|
||||||
|
if (dcRouter.certProvisionScheduler) {
|
||||||
|
const bi = await dcRouter.certProvisionScheduler.getBackoffInfo(domain);
|
||||||
|
if (bi) {
|
||||||
|
backoffInfo = bi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
certificates.push({
|
certificates.push({
|
||||||
routeName: route.name,
|
domain,
|
||||||
domains: routeDomains,
|
routeNames: info.routeNames,
|
||||||
status,
|
status,
|
||||||
source,
|
source: info.source,
|
||||||
tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
|
tlsMode: info.tlsMode,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
issuer,
|
issuer,
|
||||||
issuedAt,
|
issuedAt,
|
||||||
error,
|
error,
|
||||||
canReprovision,
|
canReprovision: info.canReprovision,
|
||||||
|
backoffInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +241,10 @@ export class CertificateHandler {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
|
/**
|
||||||
|
* Legacy route-based reprovisioning
|
||||||
|
*/
|
||||||
|
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
const smartProxy = dcRouter.smartProxy;
|
const smartProxy = dcRouter.smartProxy;
|
||||||
|
|
||||||
@@ -176,11 +254,58 @@ export class CertificateHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await smartProxy.provisionCertificate(routeName);
|
await smartProxy.provisionCertificate(routeName);
|
||||||
// Clear event-based status so it gets refreshed
|
// Clear event-based status for domains in this route
|
||||||
dcRouter.certificateStatusMap.delete(routeName);
|
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
||||||
|
if (entry.routeNames.includes(routeName)) {
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain-based reprovisioning — clears backoff first, then triggers provision
|
||||||
|
*/
|
||||||
|
private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const smartProxy = dcRouter.smartProxy;
|
||||||
|
|
||||||
|
if (!smartProxy) {
|
||||||
|
return { success: false, message: 'SmartProxy is not running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear backoff for this domain (user override)
|
||||||
|
if (dcRouter.certProvisionScheduler) {
|
||||||
|
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear status map entry so it gets refreshed
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
|
||||||
|
// Try to provision via SmartAcme directly
|
||||||
|
if (dcRouter.smartAcme) {
|
||||||
|
try {
|
||||||
|
await dcRouter.smartAcme.getCertificateForDomain(domain);
|
||||||
|
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try provisioning via the first matching route
|
||||||
|
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
||||||
|
if (routeNames.length > 0) {
|
||||||
|
try {
|
||||||
|
await smartProxy.provisionCertificate(routeNames[0]);
|
||||||
|
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisionin
|
|||||||
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
||||||
|
|
||||||
export interface ICertificateInfo {
|
export interface ICertificateInfo {
|
||||||
routeName: string;
|
domain: string;
|
||||||
domains: string[];
|
routeNames: string[];
|
||||||
status: TCertificateStatus;
|
status: TCertificateStatus;
|
||||||
source: TCertificateSource;
|
source: TCertificateSource;
|
||||||
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
@@ -15,6 +15,11 @@ export interface ICertificateInfo {
|
|||||||
issuedAt?: string; // ISO string
|
issuedAt?: string; // ISO string
|
||||||
error?: string; // if status === 'failed'
|
error?: string; // if status === 'failed'
|
||||||
canReprovision: boolean; // true for acme/provision-function routes
|
canReprovision: boolean; // true for acme/provision-function routes
|
||||||
|
backoffInfo?: {
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string; // ISO string
|
||||||
|
lastError?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
@@ -38,6 +43,7 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy route-based reprovision (kept for backward compat)
|
||||||
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IReq_ReprovisionCertificate
|
IReq_ReprovisionCertificate
|
||||||
@@ -52,3 +58,19 @@ export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfa
|
|||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Domain-based reprovision (preferred)
|
||||||
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ReprovisionCertificateDomain
|
||||||
|
> {
|
||||||
|
method: 'reprovisionCertificateDomain';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.5',
|
version: '6.0.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -719,18 +719,18 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||||
async (statePartArg, routeName) => {
|
async (statePartArg, domain) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_ReprovisionCertificate
|
interfaces.requests.IReq_ReprovisionCertificateDomain
|
||||||
>('/typedrequest', 'reprovisionCertificate');
|
>('/typedrequest', 'reprovisionCertificateDomain');
|
||||||
|
|
||||||
await request.fire({
|
await request.fire({
|
||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
routeName,
|
domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-fetch overview after reprovisioning
|
// Re-fetch overview after reprovisioning
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.domainPills {
|
.routePills {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domainPill {
|
.routePill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -125,6 +125,17 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backoffIndicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
|
||||||
|
}
|
||||||
|
|
||||||
.expiryInfo {
|
.expiryInfo {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -218,12 +229,14 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
<dees-table
|
<dees-table
|
||||||
.data=${this.certState.certificates}
|
.data=${this.certState.certificates}
|
||||||
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
||||||
Route: cert.routeName,
|
Domain: cert.domain,
|
||||||
Domains: this.renderDomainPills(cert.domains),
|
Routes: this.renderRoutePills(cert.routeNames),
|
||||||
Status: this.renderStatusBadge(cert.status),
|
Status: this.renderStatusBadge(cert.status),
|
||||||
Source: this.renderSourceBadge(cert.source),
|
Source: this.renderSourceBadge(cert.source),
|
||||||
Expires: this.renderExpiry(cert.expiryDate),
|
Expires: this.renderExpiry(cert.expiryDate),
|
||||||
Error: cert.error
|
Error: cert.backoffInfo
|
||||||
|
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
|
||||||
|
: cert.error
|
||||||
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
||||||
: '',
|
: '',
|
||||||
})}
|
})}
|
||||||
@@ -245,11 +258,11 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
}
|
}
|
||||||
await appstate.certificateStatePart.dispatchAction(
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
appstate.reprovisionCertificateAction,
|
appstate.reprovisionCertificateAction,
|
||||||
cert.routeName,
|
cert.domain,
|
||||||
);
|
);
|
||||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
DeesToast.show({
|
DeesToast.show({
|
||||||
message: `Reprovisioning triggered for ${cert.routeName}`,
|
message: `Reprovisioning triggered for ${cert.domain}`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
@@ -263,7 +276,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
await DeesModal.createAndShow({
|
await DeesModal.createAndShow({
|
||||||
heading: `Certificate: ${cert.routeName}`,
|
heading: `Certificate: ${cert.domain}`,
|
||||||
content: html`
|
content: html`
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
<dees-dataview-codebox
|
<dees-dataview-codebox
|
||||||
@@ -275,10 +288,10 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Route Name',
|
name: 'Copy Domain',
|
||||||
iconName: 'copy',
|
iconName: 'copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(cert.routeName);
|
await navigator.clipboard.writeText(cert.domain);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -287,7 +300,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
heading1="Certificate Status"
|
heading1="Certificate Status"
|
||||||
heading2="TLS certificates across all routes"
|
heading2="TLS certificates by domain"
|
||||||
searchable
|
searchable
|
||||||
.pagination=${true}
|
.pagination=${true}
|
||||||
.paginationSize=${50}
|
.paginationSize=${50}
|
||||||
@@ -296,14 +309,14 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDomainPills(domains: string[]): TemplateResult {
|
private renderRoutePills(routeNames: string[]): TemplateResult {
|
||||||
const maxShow = 3;
|
const maxShow = 3;
|
||||||
const visible = domains.slice(0, maxShow);
|
const visible = routeNames.slice(0, maxShow);
|
||||||
const remaining = domains.length - maxShow;
|
const remaining = routeNames.length - maxShow;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<span class="domainPills">
|
<span class="routePills">
|
||||||
${visible.map((d) => html`<span class="domainPill">${d}</span>`)}
|
${visible.map((r) => html`<span class="routePill">${r}</span>`)}
|
||||||
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
@@ -352,4 +365,16 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatRetryTime(retryAfter?: string): string {
|
||||||
|
if (!retryAfter) return 'soon';
|
||||||
|
const retryDate = new Date(retryAfter);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = retryDate.getTime() - now.getTime();
|
||||||
|
if (diffMs <= 0) return 'now';
|
||||||
|
const diffMin = Math.ceil(diffMs / 60000);
|
||||||
|
if (diffMin < 60) return `in ${diffMin}m`;
|
||||||
|
const diffHours = Math.ceil(diffMin / 60);
|
||||||
|
return `in ${diffHours}h`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user