Compare commits

...

8 Commits

Author SHA1 Message Date
5b6f7b30c3 v6.2.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 00:26:35 +00:00
18cc21a49e feat(ts_web): add Certificate Management documentation and ops-view-certificates reference 2026-02-16 00:26:35 +00:00
46fa2f6ade v6.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 00:22:23 +00:00
0a6315f177 feat(certs): integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler 2026-02-16 00:22:23 +00:00
841f99e19d v6.0.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-15 16:03:13 +00:00
8e9de46cd2 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. 2026-02-15 16:03:13 +00:00
2d44528345 v5.5.0
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-14 14:27:59 +00:00
28a38252da feat(certs): persist ACME certificates in StorageManager, add storage-backed cert manager, default storage to filesystem, and improve certificate status reporting 2026-02-14 14:27:58 +00:00
15 changed files with 766 additions and 313 deletions

View File

@@ -1,5 +1,43 @@
# Changelog # Changelog
## 2026-02-16 - 6.2.0 - feat(ts_web)
add Certificate Management documentation and ops-view-certificates reference
- Adds a new 'Certificate Management' section to ts_web/readme.md describing domain-centric overview, certificate sources (ACME/provision/static), expiry monitoring, per-domain backoff, and one-click reprovisioning
- Adds ops-view-certificates.ts entry to the ops UI file list
- Documents new route mapping '/certificates' in the readme navigation
## 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, oneclick 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.
- 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) ## 2026-02-14 - 5.4.6 - fix(deps)
bump @push.rocks/smartproxy dependency to ^25.2.2 bump @push.rocks/smartproxy dependency to ^25.2.2

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "5.4.6", "version": "6.2.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": {
@@ -36,20 +36,20 @@
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^8.0.0", "@push.rocks/smartacme": "^9.0.0",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.1", "@push.rocks/smartdns": "^7.8.1",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@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.2", "@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",

312
pnpm-lock.yaml generated
View File

@@ -36,8 +36,8 @@ importers:
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
'@push.rocks/smartacme': '@push.rocks/smartacme':
specifier: ^8.0.0 specifier: ^9.0.0
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7) version: 9.1.2(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.2 specifier: ^25.3.1
version: 25.2.2(@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)
@@ -154,9 +154,6 @@ packages:
peerDependencies: peerDependencies:
'@push.rocks/smartserve': '>=1.1.0' '@push.rocks/smartserve': '>=1.1.0'
'@apiclient.xyz/cloudflare@6.4.3':
resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
'@apiclient.xyz/cloudflare@7.1.0': '@apiclient.xyz/cloudflare@7.1.0':
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==} resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
@@ -651,9 +648,6 @@ packages:
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@leichtgewicht/ip-codec@2.0.5':
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
'@lit-labs/ssr-dom-shim@1.5.1': '@lit-labs/ssr-dom-shim@1.5.1':
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
@@ -858,8 +852,8 @@ packages:
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartacme@8.0.0': '@push.rocks/smartacme@9.1.2':
resolution: {integrity: sha512-Oq+m+LX4IG0p4qCGZLEwa6UlMo5Hfq7paRjpREwQNsaGSKl23xsjsEJLxjxkePwaXnaIkHEwU/5MtrEkg2uKEQ==} resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==}
'@push.rocks/smartarchive@4.2.4': '@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -901,9 +895,6 @@ packages:
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdns@6.2.2':
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
'@push.rocks/smartdns@7.8.1': '@push.rocks/smartdns@7.8.1':
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==} resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
@@ -970,8 +961,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 +1031,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.2': '@push.rocks/smartproxy@25.3.1':
resolution: {integrity: sha512-a9ztUkT2N904O3/MrLKNCqlcznDXgJf7qS7N+1Aw+QeBzxl26ofvwDcffePOSRm6BKo+q6Df9wWJ4gHoAZURLw==} 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==}
@@ -1100,6 +1091,9 @@ packages:
'@push.rocks/smarttime@4.1.1': '@push.rocks/smarttime@4.1.1':
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==} 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': '@push.rocks/smartunique@3.0.9':
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==} resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
@@ -1128,9 +1122,15 @@ packages:
'@push.rocks/taskbuffer@4.2.0': '@push.rocks/taskbuffer@4.2.0':
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==} 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': '@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==}
@@ -1754,18 +1754,12 @@ packages:
'@tsclass/tsclass@4.4.4': '@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==} resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
'@tsclass/tsclass@5.0.0':
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
'@tsclass/tsclass@9.3.0': '@tsclass/tsclass@9.3.0':
resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==} resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==}
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/bn.js@5.2.0':
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -1784,12 +1778,6 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 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': '@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
@@ -1847,10 +1835,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==}
@@ -2097,9 +2081,6 @@ packages:
bintrees@1.0.2: bintrees@1.0.2:
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=} resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
bn.js@4.12.2:
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
body-parser@2.2.2: body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2120,9 +2101,6 @@ packages:
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==} resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
brorand@1.1.0:
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
bson@6.10.4: bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'} engines: {node: '>=16.20.1'}
@@ -2282,6 +2260,10 @@ packages:
crelt@1.0.6: crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
croner@10.0.1:
resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
engines: {node: '>=18.0'}
croner@9.1.0: croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'} engines: {node: '>=18.0'}
@@ -2375,10 +2357,6 @@ packages:
devtools-protocol@0.0.1566079: devtools-protocol@0.0.1566079:
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==} resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
engines: {node: '>=6'}
dom-serializer@2.0.0: dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -2408,9 +2386,6 @@ packages:
ee-first@1.1.1: ee-first@1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
elliptic@6.6.1:
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2743,9 +2718,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hash.js@1.1.7:
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
hasown@2.0.2: hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2767,9 +2739,6 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
hmac-drbg@1.0.1:
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
html-minifier@4.0.0: html-minifier@4.0.0:
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3280,12 +3249,6 @@ packages:
mingo@7.2.0: mingo@7.2.0:
resolution: {integrity: sha512-UeX942qZpofn5L97h295SkS7j/ADf7Qac8gdRCMBPxi0/1m70aeB2owLFvWbyuMj1dowonlivlVRQVDx+6h+7Q==} 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: minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -4379,7 +4342,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 +4391,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
@@ -4492,22 +4455,10 @@ snapshots:
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.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.10
'@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': '@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 +5192,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 +5213,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 +5239,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 +5259,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 +5274,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 +5290,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 +5320,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
@@ -5500,8 +5452,6 @@ snapshots:
'@isaacs/cliui@9.0.0': {} '@isaacs/cliui@9.0.0': {}
'@leichtgewicht/ip-codec@2.0.5': {}
'@lit-labs/ssr-dom-shim@1.5.1': {} '@lit-labs/ssr-dom-shim@1.5.1': {}
'@lit/reactive-element@2.1.2': '@lit/reactive-element@2.1.2':
@@ -5805,7 +5755,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,34 +5779,29 @@ 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@9.1.2(socks@2.8.7)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1) '@apiclient.xyz/cloudflare': 7.1.0
'@apiclient.xyz/cloudflare': 6.4.3 '@peculiar/x509': 1.14.3
'@push.rocks/lik': 6.2.2 '@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/smartdelay': 3.0.5
'@push.rocks/smartdns': 6.2.2 '@push.rocks/smartdns': 7.8.1
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartnetwork': 4.4.0 '@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/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 6.1.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
acme-client: 5.4.0
transitivePeerDependencies: transitivePeerDependencies:
- '@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 +5811,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 +5900,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 +5925,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 +5954,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
@@ -6039,22 +5983,6 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@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': '@push.rocks/smartdns@7.8.1':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -6218,7 +6146,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 +6156,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 +6193,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 +6266,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 +6369,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.2.2(@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 +6453,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 +6488,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
@@ -6656,6 +6552,17 @@ snapshots:
is-nan: 1.3.2 is-nan: 1.3.2
pretty-ms: 9.3.0 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': '@push.rocks/smartunique@3.0.9':
dependencies: dependencies:
'@types/uuid': 9.0.8 '@types/uuid': 9.0.8
@@ -6696,7 +6603,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 +6619,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
@@ -6723,6 +6630,22 @@ snapshots:
- supports-color - supports-color
- vue - 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': '@push.rocks/webrequest@3.0.37':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -6731,6 +6654,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
@@ -7450,10 +7381,6 @@ snapshots:
dependencies: dependencies:
type-fest: 4.41.0 type-fest: 4.41.0
'@tsclass/tsclass@5.0.0':
dependencies:
type-fest: 4.41.0
'@tsclass/tsclass@9.3.0': '@tsclass/tsclass@9.3.0':
dependencies: dependencies:
type-fest: 4.41.0 type-fest: 4.41.0
@@ -7463,10 +7390,6 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@types/bn.js@5.2.0':
dependencies:
'@types/node': 25.2.3
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
@@ -7491,14 +7414,6 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@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': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.2.3
@@ -7570,10 +7485,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':
@@ -7819,8 +7730,6 @@ snapshots:
bintrees@1.0.2: {} bintrees@1.0.2: {}
bn.js@4.12.2: {}
body-parser@2.2.2: body-parser@2.2.2:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@@ -7857,8 +7766,6 @@ snapshots:
p-queue: 6.6.2 p-queue: 6.6.2
unload: 2.4.1 unload: 2.4.1
brorand@1.1.0: {}
bson@6.10.4: {} bson@6.10.4: {}
bson@7.2.0: {} bson@7.2.0: {}
@@ -8009,6 +7916,8 @@ snapshots:
crelt@1.0.6: {} crelt@1.0.6: {}
croner@10.0.1: {}
croner@9.1.0: {} croner@9.1.0: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
@@ -8081,10 +7990,6 @@ snapshots:
devtools-protocol@0.0.1566079: {} devtools-protocol@0.0.1566079: {}
dns-packet@5.6.1:
dependencies:
'@leichtgewicht/ip-codec': 2.0.5
dom-serializer@2.0.0: dom-serializer@2.0.0:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@@ -8121,16 +8026,6 @@ snapshots:
ee-first@1.1.1: {} 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@8.0.0: {}
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
@@ -8560,11 +8455,6 @@ snapshots:
dependencies: dependencies:
has-symbols: 1.1.0 has-symbols: 1.1.0
hash.js@1.1.7:
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
hasown@2.0.2: hasown@2.0.2:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
@@ -8597,12 +8487,6 @@ snapshots:
highlight.js@11.11.1: {} 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: html-minifier@4.0.0:
dependencies: dependencies:
camel-case: 3.0.0 camel-case: 3.0.0
@@ -9311,10 +9195,6 @@ snapshots:
mingo@7.2.0: {} mingo@7.2.0: {}
minimalistic-assert@1.0.1: {}
minimalistic-crypto-utils@1.0.1: {}
minimatch@10.1.2: minimatch@10.1.2:
dependencies: dependencies:
'@isaacs/brace-expansion': 5.0.1 '@isaacs/brace-expansion': 5.0.1

102
readme.md
View File

@@ -21,6 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [Email System](#email-system) - [Email System](#email-system)
- [DNS Server](#dns-server) - [DNS Server](#dns-server)
- [RADIUS Server](#radius-server) - [RADIUS Server](#radius-server)
- [Certificate Management](#certificate-management)
- [Storage & Caching](#storage--caching) - [Storage & Caching](#storage--caching)
- [Security Features](#security-features) - [Security Features](#security-features)
- [OpsServer Dashboard](#opsserver-dashboard) - [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 - **Hierarchical rate limiting** — global, per-domain, per-sender
### 🔒 Enterprise Security ### 🔒 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 - **IP reputation checking** with caching and configurable thresholds
- **Content scanning** for spam, viruses, and malicious attachments - **Content scanning** for spam, viruses, and malicious attachments
- **Security event logging** with structured audit trails - **Security event logging** with structured audit trails
@@ -73,7 +76,8 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🖥️ OpsServer Dashboard ### 🖥️ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring - **Web-based management interface** with real-time monitoring
- **JWT authentication** with session persistence - **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 - **Read-only configuration display** — DcRouter is configured through code
## Installation ## Installation
@@ -250,7 +254,7 @@ graph TB
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>] ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
DS[SmartDNS Server<br/><i>Rust-powered</i>] DS[SmartDNS Server<br/><i>Rust-powered</i>]
RS[SmartRadius Server] RS[SmartRadius Server]
CM[Certificate Manager] CM[Certificate Manager<br/><i>smartacme v9</i>]
OS[OpsServer Dashboard] OS[OpsServer Dashboard]
MM[Metrics Manager] MM[Metrics Manager]
SM[Storage 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) | | **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) | | **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) | | **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 | | **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 | | **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) | | **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: 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. 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. 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 reverse order. 3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
### Rust-Powered Architecture ### Rust-Powered Architecture
@@ -584,15 +589,6 @@ match: { sizeRange: { min: 1000, max: 5000000 }, hasAttachments: true }
match: { subject: /invoice|receipt/i } 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 ### Email Security Stack
- **DKIM** — Automatic key generation, signing, and rotation for all domains - **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 - Session monitoring and forced disconnects
- Accounting summaries and statistics - 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 ## Storage & Caching
### StorageManager ### StorageManager
@@ -725,7 +788,7 @@ storage: {
// Simply omit the storage config // 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 ### 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 | | 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics | | 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents | | 📧 **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 | | 📜 **Logs** | Real-time log viewer with level filtering and search |
| ⚙️ **Configuration** | Read-only view of current system configuration | | ⚙️ **Configuration** | Read-only view of current system configuration |
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections | | 🛡️ **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 'getBounceRecords' // Bounce records
'removeFromSuppressionList' // Unsuppress an address 'removeFromSuppressionList' // Unsuppress an address
// Certificates
'getCertificateOverview' // Domain-centric certificate status
'reprovisionCertificate' // Reprovision by route name (legacy)
'reprovisionCertificateDomain' // Reprovision by domain (preferred)
// Configuration (read-only) // Configuration (read-only)
'getConfiguration' // Current system config 'getConfiguration' // Current system config
@@ -884,6 +953,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|----------|------|-------------| |----------|------|-------------|
| `options` | `IDcRouterOptions` | Current configuration | | `options` | `IDcRouterOptions` | Current configuration |
| `smartProxy` | `SmartProxy` | SmartProxy instance | | `smartProxy` | `SmartProxy` | SmartProxy instance |
| `smartAcme` | `SmartAcme` | SmartAcme v9 certificate manager instance |
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) | | `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
| `dnsServer` | `DnsServer` | DNS server instance | | `dnsServer` | `DnsServer` | DNS server instance |
| `radiusServer` | `RadiusServer` | RADIUS server instance | | `radiusServer` | `RadiusServer` | RADIUS server instance |
@@ -891,6 +961,8 @@ const router = new DcRouter(options: IDcRouterOptions);
| `opsServer` | `OpsServer` | OpsServer/dashboard instance | | `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector | | `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance | | `cacheDb` | `CacheDb` | Cache database instance |
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
### Re-exported Types ### Re-exported Types

View File

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

View File

@@ -0,0 +1,130 @@
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
*
* Note: Serial stagger queue was removed — smartacme v9 handles
* concurrency, per-domain dedup, and rate limiting internally.
*/
export class CertProvisionScheduler {
private storageManager: StorageManager;
private maxBackoffHours: number;
// In-memory backoff cache (mirrors storage for fast lookups)
private backoffCache = new Map<string, IBackoffEntry>();
constructor(
storageManager: StorageManager,
options?: { maxBackoffHours?: number }
) {
this.storageManager = storageManager;
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,
};
}
}

View File

@@ -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 per-domain backoff
public certProvisionScheduler?: CertProvisionScheduler;
// TypedRouter for API endpoints // TypedRouter for API endpoints
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -204,7 +209,14 @@ export class DcRouter {
this.options = { this.options = {
...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,29 +449,61 @@ 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 {
// smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
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);
if (cert.validUntil) { if (cert.validUntil) {
eventComms.setExpiryDate(new Date(cert.validUntil)); eventComms.setExpiryDate(new Date(cert.validUntil));
} }
return { const result = {
id: cert.id, id: cert.id,
domainName: cert.domainName, domainName: cert.domainName,
created: cert.created, created: cert.created,
@@ -468,7 +512,13 @@ 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 +542,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 +742,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 +758,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...');

View 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);
}
}
}

View File

@@ -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}'` };
}
} }

View File

@@ -128,6 +128,37 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records | | `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address | | `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 #### 📡 RADIUS
| Interface | Method | Description | | Interface | Method | Description |
|-----------|--------|-------------| |-----------|--------|-------------|
@@ -173,7 +204,16 @@ console.log('Email:', metrics.emailStats);
console.log('DNS:', metrics.dnsStats); console.log('DNS:', metrics.dnsStats);
console.log('Security:', metrics.securityMetrics); console.log('Security:', metrics.securityMetrics);
// 3. Check email queues // 3. Check certificate status
const certClient = new typedrequest.TypedRequest<requests.IReq_GetCertificateOverview>(
'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<requests.IReq_GetQueuedEmails>( const queueClient = new typedrequest.TypedRequest<requests.IReq_GetQueuedEmails>(
'https://your-dcrouter:3000/typedrequest', 'https://your-dcrouter:3000/typedrequest',
'getQueuedEmails' 'getQueuedEmails'

View File

@@ -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;
};
}

View File

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

View File

@@ -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

View File

@@ -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,14 +229,16 @@ 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="errorText" title="${cert.error}">${cert.error}</span>` ? 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>`
: '',
})} })}
.dataActions=${[ .dataActions=${[
{ {
@@ -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`;
}
} }

View File

@@ -34,6 +34,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Security** — Security incidents from email processing - **Security** — Security incidents from email processing
- Bounce record management and suppression list controls - Bounce record management and suppression list controls
### 🔐 Certificate Management
- Domain-centric certificate overview with status indicators
- Certificate source tracking (ACME, provision function, static)
- Expiry date monitoring and alerts
- Per-domain backoff status for failed provisions
- One-click reprovisioning per domain
### 📜 Log Viewer ### 📜 Log Viewer
- Real-time log streaming - Real-time log streaming
- Filter by log level (error, warning, info, debug) - Filter by log level (error, warning, info, debug)
@@ -77,6 +84,7 @@ ts_web/
├── ops-view-overview.ts # Overview statistics ├── ops-view-overview.ts # Overview statistics
├── ops-view-network.ts # Network monitoring ├── ops-view-network.ts # Network monitoring
├── ops-view-emails.ts # Email queue management ├── ops-view-emails.ts # Email queue management
├── ops-view-certificates.ts # Certificate overview & reprovisioning
├── ops-view-logs.ts # Log viewer ├── ops-view-logs.ts # Log viewer
├── ops-view-config.ts # Configuration display ├── ops-view-config.ts # Configuration display
├── ops-view-security.ts # Security dashboard ├── ops-view-security.ts # Security dashboard
@@ -132,6 +140,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
/emails/sent → Sent emails /emails/sent → Sent emails
/emails/failed → Failed emails /emails/failed → Failed emails
/emails/security → Security incidents /emails/security → Security incidents
/certificates → Certificate management
/logs → Log viewer /logs → Log viewer
/configuration → System configuration /configuration → System configuration
/security → Security dashboard /security → Security dashboard