Compare commits

...

33 Commits

Author SHA1 Message Date
cd957526e2 v6.2.1
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:56:41 +00:00
7aa5f07731 fix(smartacme,storage): Respect wildcard domain requests when retrieving certificates and treat empty/whitespace storage values as null in getJSON 2026-02-16 00:56:41 +00:00
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
dfb268bbfc v5.4.6
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 12:49:57 +00:00
6532c7ff22 fix(deps): bump @push.rocks/smartproxy dependency to ^25.2.2 2026-02-14 12:49:57 +00:00
d2c63cf170 v5.4.5
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 12:33:04 +00:00
09d66e4528 fix(dcrouter): bump patch for release pipeline consistency - no code changes 2026-02-14 12:33:04 +00:00
3078fa9d7b feat(dashboard): use SmartProxy server-side throughput history and per-IP bandwidth in network view 2026-02-14 12:31:44 +00:00
57fbb128e6 v5.4.4
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 11:26:58 +00:00
d73266eeb8 fix(deps): bump @push.rocks/smartproxy to ^25.2.0 2026-02-14 11:26:58 +00:00
2dbdf2d2b1 v5.4.3
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 09:25:59 +00:00
383e0adc23 fix(dependencies): bump @push.rocks/smartproxy to ^25.1.0 2026-02-14 09:25:59 +00:00
d7789f5a44 v5.4.2
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-13 23:16:25 +00:00
2638990667 fix(dcrouter): improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes 2026-02-13 23:16:25 +00:00
c33ecdc26f v5.4.1
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-13 22:03:23 +00:00
b033d80927 fix(network,dcrouter): Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI 2026-02-13 22:03:23 +00:00
cf5d616769 v5.4.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-13 21:37:52 +00:00
8e722f5ab6 feat(certificates): include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information 2026-02-13 21:37:52 +00:00
2b75709161 v5.3.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-13 17:05:33 +00:00
c5e2c262b7 feat(certificates): add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events 2026-02-13 17:05:33 +00:00
d10896196d v5.2.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-13 14:19:19 +00:00
8be1e87bdc feat(monitoring): add throughput metrics and expose them in ops UI 2026-02-13 14:19:19 +00:00
96cefe984a v5.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-13 12:12:01 +00:00
ca112c3e42 feat(acme): Integrate SmartAcme DNS-01 handling and add certificate provisioning for SmartProxy 2026-02-13 12:12:01 +00:00
85b6c4fa51 v5.0.7
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-13 00:02:09 +00:00
ee550e6f25 fix(deps): bump @push.rocks/smartdns to ^7.8.1 and @push.rocks/smartmta to ^5.2.2 2026-02-13 00:02:09 +00:00
28 changed files with 1864 additions and 388 deletions

View File

@@ -1,5 +1,134 @@
# Changelog
## 2026-02-16 - 6.2.1 - fix(smartacme,storage)
Respect wildcard domain requests when retrieving certificates and treat empty/whitespace storage values as null in getJSON
- Pass includeWildcard flag to smartAcme.getCertificateForDomain to avoid incorrectly including/excluding wildcard certificates based on whether the requested domain itself is a wildcard
- Detect wildcard domains via domain.startsWith('*.') and set includeWildcard to false for wildcard requests
- Treat empty or whitespace-only stored values as null in StorageManager.getJSON to avoid parsing empty strings as JSON and potential errors
## 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)
bump @push.rocks/smartproxy dependency to ^25.2.2
- Updated dependency @push.rocks/smartproxy: ^25.2.0 → ^25.2.2
- Change is a dependency-only patch update, no source code modifications
- Current package version is 5.4.5; recommend a patch release
## 2026-02-14 - 5.4.5 - fix(dcrouter)
bump patch for release pipeline consistency - no code changes
- current version: 5.4.4 (from package.json)
- git diff: no changes detected
- recommend patch bump to trigger release artifacts if required
## 2026-02-14 - 5.4.4 - fix(deps)
bump @push.rocks/smartproxy to ^25.2.0
- Updated @push.rocks/smartproxy from ^25.1.0 to ^25.2.0 (patch, non-breaking).
- Current package version is 5.4.3; recommend a patch release to 5.4.4.
## 2026-02-14 - 5.4.3 - fix(dependencies)
bump @push.rocks/smartproxy to ^25.1.0
- Updated @push.rocks/smartproxy from ^25.0.0 to ^25.1.0 in package.json
## 2026-02-13 - 5.4.2 - fix(dcrouter)
improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes
- Support routing-glob patterns beginning with '*' (e.g. *example.com) to match base domain, wildcard form, and subdomains
- Treat standard wildcard patterns ('*.example.com') as matching both the base domain (example.com) and its subdomains
- Use isDomainMatch when resolving routes instead of exact array includes to allow pattern matching
- Normalize domain and pattern to lowercase and simplify equality checks
## 2026-02-13 - 5.4.1 - fix(network,dcrouter)
Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI
- Always register SmartProxy 'certificate-issued', 'certificate-renewed', and 'certificate-failed' handlers (previously only registered when acmeConfig was present) so certificate events are processed regardless of provisioning path.
- Add totalBytes (in/out) to network stats and propagate it through ts_interfaces and app state so total data transferred is available to the UI.
- Combine metricsManager.getNetworkStats with collectServerStats to compute activeConnections and adjust connectionDetails/TopEndpoints handling.
- Update ops UI to display totalBytes in throughput cards and remove a redundant network-specific auto-refresh fetch.
- Type and state updates: ts_interfaces/data/stats.ts and ts_web/appstate.ts updated with totalBytes and initialization/default mapping adjusted.
## 2026-02-13 - 5.4.0 - feat(certificates)
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
- bump @push.rocks/smartproxy dependency to ^25.0.0
- add optional 'source' field to certificate status and propagate event.source when certificates are issued, renewed, or failed
- change smartProxy.certProvisionFunction signature to accept eventComms; use it to log attempts, set source and expiryDate, and fall back to http-01 on DNS-01 failure
- make buildCertificateOverview async and query smartProxy.getCertificateStatus for a route when event-based status is unknown
- improve logging to include certificate source and more contextual messages
## 2026-02-13 - 5.3.0 - feat(certificates)
add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events
- Add CertificateHandler with typedrequest endpoints: getCertificateOverview and reprovisionCertificate
- Introduce ICertificateInfo and request/response interfaces for certificate operations
- Frontend: add certificate state part, actions (fetchCertificateOverview, reprovisionCertificate), router view, and ops-view-certificates component
- DcRouter: add certificateStatusMap, listen to SmartProxy certificate-issued/renewed/failed events, and add findRouteNameForDomain helper
- Bump dependency @push.rocks/smartproxy to ^24.0.0
## 2026-02-13 - 5.2.0 - feat(monitoring)
add throughput metrics and expose them in ops UI
- MetricsManager now reports bytesInPerSecond and bytesOutPerSecond as part of throughput
- Extended IServerStats with requestsPerSecond and throughput {bytesIn, bytesOut, bytesInPerSecond, bytesOutPerSecond}
- Stats handler updated to include requestsPerSecond and throughput; fallback stats initialize throughput fields to zero
- Web UI ops overview displays Throughput In/Out (bits/s) and total bytes with new formatting helper
- Bumped dependency @push.rocks/smartproxy to ^23.1.6
## 2026-02-13 - 5.1.0 - feat(acme)
Integrate SmartAcme DNS-01 handling and add certificate provisioning for SmartProxy
- Add smartAcme property and lifecycle management (start/stop) in DcRouter
- Create SmartAcme instance when DNS challenge handlers are present and wire certProvisionFunction to SmartProxy to return certificates for domains
- Fall back to http-01 provisioning on SmartAcme errors for a domain
- Stop SmartAcme during shutdown sequence to clean up resources
- Bump dependency @push.rocks/smartproxy to ^23.1.5
## 2026-02-13 - 5.0.7 - fix(deps)
bump @push.rocks/smartdns to ^7.8.1 and @push.rocks/smartmta to ^5.2.2
- package.json: updated @push.rocks/smartdns from ^7.8.0 to ^7.8.1 (patch)
- package.json: updated @push.rocks/smartmta from ^5.2.1 to ^5.2.2 (patch)
## 2026-02-12 - 5.0.6 - fix(deps)
bump @push.rocks/smartproxy to ^23.1.4

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "5.0.6",
"version": "6.2.1",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -36,20 +36,20 @@
"@design.estate/dees-element": "^2.1.6",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^8.0.0",
"@push.rocks/smartacme": "^9.0.0",
"@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.0",
"@push.rocks/smartdns": "^7.8.1",
"@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0",
"@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/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.2.1",
"@push.rocks/smartmta": "^5.2.2",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^23.1.4",
"@push.rocks/smartproxy": "^25.3.1",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",

356
pnpm-lock.yaml generated
View File

@@ -36,14 +36,14 @@ importers:
specifier: ^6.1.3
version: 6.1.3
'@push.rocks/smartacme':
specifier: ^8.0.0
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
specifier: ^9.0.0
version: 9.1.2(socks@2.8.7)
'@push.rocks/smartdata':
specifier: ^7.0.15
version: 7.0.15(socks@2.8.7)
'@push.rocks/smartdns':
specifier: ^7.8.0
version: 7.8.0
specifier: ^7.8.1
version: 7.8.1
'@push.rocks/smartfile':
specifier: ^13.1.2
version: 13.1.2
@@ -54,8 +54,8 @@ importers:
specifier: ^2.2.1
version: 2.2.1
'@push.rocks/smartlog':
specifier: ^3.1.10
version: 3.1.10
specifier: ^3.1.11
version: 3.1.11
'@push.rocks/smartmetrics':
specifier: ^2.0.10
version: 2.0.10
@@ -63,8 +63,8 @@ importers:
specifier: ^5.1.0
version: 5.1.0(socks@2.8.7)
'@push.rocks/smartmta':
specifier: ^5.2.1
version: 5.2.1
specifier: ^5.2.2
version: 5.2.2
'@push.rocks/smartnetwork':
specifier: ^4.4.0
version: 4.4.0
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3
version: 4.2.3
'@push.rocks/smartproxy':
specifier: ^23.1.4
version: 23.1.4(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
specifier: ^25.3.1
version: 25.3.1
'@push.rocks/smartradius':
specifier: ^1.1.1
version: 1.1.1
@@ -116,7 +116,7 @@ importers:
version: 2.0.1
'@git.zone/tstest':
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':
specifier: ^3.1.0
version: 3.1.0(@tiptap/pm@2.27.2)
@@ -154,9 +154,6 @@ packages:
peerDependencies:
'@push.rocks/smartserve': '>=1.1.0'
'@apiclient.xyz/cloudflare@6.4.3':
resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
'@apiclient.xyz/cloudflare@7.1.0':
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
@@ -651,9 +648,6 @@ packages:
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'}
'@leichtgewicht/ip-codec@2.0.5':
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
'@lit-labs/ssr-dom-shim@1.5.1':
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
@@ -858,8 +852,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartacme@8.0.0':
resolution: {integrity: sha512-Oq+m+LX4IG0p4qCGZLEwa6UlMo5Hfq7paRjpREwQNsaGSKl23xsjsEJLxjxkePwaXnaIkHEwU/5MtrEkg2uKEQ==}
'@push.rocks/smartacme@9.1.2':
resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -901,11 +895,8 @@ packages:
'@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdns@6.2.2':
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
'@push.rocks/smartdns@7.8.0':
resolution: {integrity: sha512-5FX74AAgQSqWPZkpTsI/BbUKBQpZKSvs+UdX9IZpwcuPldI+K7D1WeE02mMAGd1Ncd/sYAMor5CTlhnG6L+QhQ==}
'@push.rocks/smartdns@7.8.1':
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
'@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
@@ -970,8 +961,8 @@ packages:
'@push.rocks/smartlog-interfaces@3.0.2':
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
'@push.rocks/smartlog@3.1.10':
resolution: {integrity: sha512-5pf5JyzOE2WTCUislNIW4EHePo1a7hiXB+jbil38+N5hW71AEwcPFe6oGxbp5w9ALlz66hV2+E+25R0SsxN+fQ==}
'@push.rocks/smartlog@3.1.11':
resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==}
'@push.rocks/smartmail@2.2.0':
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
@@ -1000,8 +991,8 @@ packages:
'@push.rocks/smartmongo@5.1.0':
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
'@push.rocks/smartmta@5.2.1':
resolution: {integrity: sha512-ITgu1kIJxWgiU6q3YDxAp1HoMmC8ECJhEAFbDtUDRIBcg8Flvbmgasjnqew67nFcXq2fKYh3rGECloS62MBQgw==}
'@push.rocks/smartmta@5.2.2':
resolution: {integrity: sha512-0xKUi2BMM0HFYIPdNeNJZFitAiJ9CNbLlOJ8TenT+xInp7DKcSQ7ABER1rJKinPtvDjRDSiSqiF2iQR+O7299g==}
engines: {node: '>=14.0.0'}
cpu: [x64, arm64]
os: [darwin, linux, win32]
@@ -1040,8 +1031,8 @@ packages:
'@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@23.1.4':
resolution: {integrity: sha512-VzpXVw3VsA7muhqkEB95pxCFtKtNLLjNCQcdvf0s49TVPXy/wHcmMqOPmExacLZEuvzXYiRM5poUqX4+em/8zw==}
'@push.rocks/smartproxy@25.3.1':
resolution: {integrity: sha512-kGJGpx3KBUz+qWU2L9B2gbZoUbQEG2BFe6ZzK0b68Y32nHoSIMjol14hzc3sRgW1p/loWy+Gj+5j0KuVytKWmA==}
'@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1061,9 +1052,6 @@ packages:
'@push.rocks/smartrouter@1.3.3':
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
'@push.rocks/smartrust@1.2.0':
resolution: {integrity: sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==}
'@push.rocks/smartrust@1.2.1':
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
@@ -1103,6 +1091,9 @@ packages:
'@push.rocks/smarttime@4.1.1':
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
'@push.rocks/smarttime@4.2.3':
resolution: {integrity: sha512-8gMg8RUkrCG4p9NcEUZV7V6KpL24+jAMK02g7qyhfA6giz/JJWD0+8w8xjSR+G7qe16KVQ2y3RbvAL9TxmO36g==}
'@push.rocks/smartunique@3.0.9':
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
@@ -1131,9 +1122,15 @@ packages:
'@push.rocks/taskbuffer@4.2.0':
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
'@push.rocks/taskbuffer@6.1.2':
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
'@push.rocks/webrequest@3.0.37':
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
'@push.rocks/webrequest@4.0.1':
resolution: {integrity: sha512-I60XZZLVf8W5I7YdmUVVu4G92teE3rg3/aKaV00BRg8vJ3VXx3wc59Qj4em7zxQ5o0HvL8m1Aezw3RFMDPyVgA==}
'@push.rocks/webrequest@4.0.2':
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
@@ -1757,18 +1754,12 @@ packages:
'@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
'@tsclass/tsclass@5.0.0':
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
'@tsclass/tsclass@9.3.0':
resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/bn.js@5.2.0':
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -1787,12 +1778,6 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/dns-packet@5.6.5':
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
'@types/elliptic@6.4.18':
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
'@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
@@ -1850,10 +1835,6 @@ packages:
'@types/minimatch@5.1.2':
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':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -2100,9 +2081,6 @@ packages:
bintrees@1.0.2:
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
bn.js@4.12.2:
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
@@ -2123,9 +2101,6 @@ packages:
broadcast-channel@7.3.0:
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
brorand@1.1.0:
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
@@ -2285,6 +2260,10 @@ packages:
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
croner@10.0.1:
resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
engines: {node: '>=18.0'}
croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'}
@@ -2378,10 +2357,6 @@ packages:
devtools-protocol@0.0.1566079:
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
engines: {node: '>=6'}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -2411,9 +2386,6 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
elliptic@6.6.1:
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2746,9 +2718,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hash.js@1.1.7:
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -2770,9 +2739,6 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hmac-drbg@1.0.1:
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
html-minifier@4.0.0:
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
engines: {node: '>=6'}
@@ -3283,12 +3249,6 @@ packages:
mingo@7.2.0:
resolution: {integrity: sha512-UeX942qZpofn5L97h295SkS7j/ADf7Qac8gdRCMBPxi0/1m70aeB2owLFvWbyuMj1dowonlivlVRQVDx+6h+7Q==}
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
minimalistic-crypto-utils@1.0.1:
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22}
@@ -4247,7 +4207,7 @@ packages:
hasBin: true
wordwrap@1.0.0:
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
@@ -4382,7 +4342,7 @@ snapshots:
'@push.rocks/smartfeed': 1.4.0
'@push.rocks/smartfile': 11.2.7
'@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-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2
@@ -4431,7 +4391,7 @@ snapshots:
'@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1
'@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-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2
@@ -4495,22 +4455,10 @@ snapshots:
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
'@apiclient.xyz/cloudflare@6.4.3':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.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':
dependencies:
'@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/smartrequest': 5.0.1
'@push.rocks/smartstring': 4.1.0
@@ -5244,7 +5192,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2
'@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/smartpromise': 4.2.3
typescript: 5.9.3
@@ -5265,7 +5213,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1
'@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/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
@@ -5291,7 +5239,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2
'@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/smartpath': 6.0.0
'@push.rocks/smartrequest': 5.0.1
@@ -5311,7 +5259,7 @@ snapshots:
'@push.rocks/smartshell': 3.3.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:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
'@git.zone/tsbundle': 2.8.3
@@ -5326,7 +5274,7 @@ snapshots:
'@push.rocks/smartexpect': 2.5.0
'@push.rocks/smartfile': 11.2.7
'@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/smartnetwork': 4.4.0
'@push.rocks/smartpath': 6.0.0
@@ -5342,6 +5290,7 @@ snapshots:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- '@nuxt/kit'
- '@push.rocks/smartserve'
- '@swc/helpers'
- aws-crt
- bare-abort-controller
@@ -5371,7 +5320,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1
'@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/smartshell': 3.3.0
'@push.rocks/smartwatch': 6.3.0
@@ -5503,8 +5452,6 @@ snapshots:
'@isaacs/cliui@9.0.0': {}
'@leichtgewicht/ip-codec@2.0.5': {}
'@lit-labs/ssr-dom-shim@1.5.1': {}
'@lit/reactive-element@2.1.2':
@@ -5808,7 +5755,7 @@ snapshots:
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartfile': 11.2.7
'@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/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
@@ -5832,34 +5779,29 @@ snapshots:
'@api.global/typedrequest': 3.2.6
'@configvault.io/interfaces': 1.0.17
'@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/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:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
'@apiclient.xyz/cloudflare': 6.4.3
'@apiclient.xyz/cloudflare': 7.1.0
'@peculiar/x509': 1.14.3
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdata': 5.16.7(socks@2.8.7)
'@push.rocks/smartdata': 7.0.15(socks@2.8.7)
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 6.2.2
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartdns': 7.8.1
'@push.rocks/smartlog': 3.1.11
'@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 6.1.2
'@tsclass/tsclass': 9.3.0
acme-client: 5.4.0
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- '@nuxt/kit'
- '@push.rocks/smartserve'
- bare-abort-controller
- bufferutil
- encoding
- gcp-metadata
- kerberos
@@ -5869,7 +5811,6 @@ snapshots:
- snappy
- socks
- supports-color
- utf-8-validate
- vue
'@push.rocks/smartarchive@4.2.4':
@@ -5959,7 +5900,7 @@ snapshots:
'@push.rocks/smartcli@4.0.20':
dependencies:
'@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/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
@@ -5984,7 +5925,7 @@ snapshots:
dependencies:
'@push.rocks/lik': 6.2.2
'@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/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
@@ -6013,7 +5954,7 @@ snapshots:
dependencies:
'@push.rocks/lik': 6.2.2
'@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/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
@@ -6042,34 +5983,15 @@ snapshots:
dependencies:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartdns@6.2.2':
'@push.rocks/smartdns@7.8.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartenv': 6.0.0
'@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.0':
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
'@push.rocks/smartrust': 1.2.0
'@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0
'@types/dns-packet': 5.6.5
acme-client: 5.4.0
dns-packet: 5.6.1
minimatch: 10.1.2
minimatch: 10.2.0
transitivePeerDependencies:
- supports-color
@@ -6224,7 +6146,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 2.0.2
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartlog@3.1.10':
'@push.rocks/smartlog@3.1.11':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/consolecolor': 2.0.3
@@ -6234,12 +6156,12 @@ snapshots:
'@push.rocks/smarthash': 3.2.6
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarttime': 4.1.1
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webrequest': 4.0.1
'@tsclass/tsclass': 9.3.0
'@push.rocks/smartmail@2.2.0':
dependencies:
'@push.rocks/smartdns': 7.8.0
'@push.rocks/smartdns': 7.8.1
'@push.rocks/smartfile': 13.1.2
'@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpath': 6.0.0
@@ -6271,7 +6193,7 @@ snapshots:
'@push.rocks/smartmetrics@2.0.10':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartlog': 3.1.11
'@types/pidusage': 2.0.5
pidtree: 0.6.0
pidusage: 4.0.1
@@ -6340,14 +6262,14 @@ snapshots:
- supports-color
- vue
'@push.rocks/smartmta@5.2.1':
'@push.rocks/smartmta@5.2.2':
dependencies:
'@push.rocks/smartfile': 13.1.2
'@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/smartpath': 6.0.0
'@push.rocks/smartrust': 1.2.0
'@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0
lru-cache: 11.2.6
mailparser: 3.9.3
@@ -6361,7 +6283,7 @@ snapshots:
'@push.rocks/smartnetwork@4.4.0':
dependencies:
'@push.rocks/smartdns': 7.8.0
'@push.rocks/smartdns': 7.8.1
'@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0
@@ -6447,45 +6369,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@23.1.4(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
'@push.rocks/smartproxy@25.3.1':
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/smartdelay': 3.0.5
'@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/smartlog': 3.1.11
'@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
'@types/minimatch': 6.0.0
'@types/ws': 8.18.1
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)':
dependencies:
@@ -6538,10 +6428,6 @@ snapshots:
'@push.rocks/smartrx': 3.0.10
path-to-regexp: 8.3.0
'@push.rocks/smartrust@1.2.0':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust@1.2.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
@@ -6567,7 +6453,7 @@ snapshots:
'@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0
ws: 8.19.0
transitivePeerDependencies:
@@ -6602,7 +6488,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13
'@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/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1
@@ -6666,6 +6552,17 @@ snapshots:
is-nan: 1.3.2
pretty-ms: 9.3.0
'@push.rocks/smarttime@4.2.3':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3
croner: 10.0.1
date-fns: 4.1.0
dayjs: 1.11.19
is-nan: 1.3.2
pretty-ms: 9.3.0
'@push.rocks/smartunique@3.0.9':
dependencies:
'@types/uuid': 9.0.8
@@ -6706,7 +6603,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2
'@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/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1
@@ -6722,7 +6619,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2
'@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/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1
@@ -6733,6 +6630,22 @@ snapshots:
- supports-color
- vue
'@push.rocks/taskbuffer@6.1.2':
dependencies:
'@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@push.rocks/webrequest@3.0.37':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -6741,6 +6654,14 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@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':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -7460,10 +7381,6 @@ snapshots:
dependencies:
type-fest: 4.41.0
'@tsclass/tsclass@5.0.0':
dependencies:
type-fest: 4.41.0
'@tsclass/tsclass@9.3.0':
dependencies:
type-fest: 4.41.0
@@ -7473,10 +7390,6 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/bn.js@5.2.0':
dependencies:
'@types/node': 25.2.3
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@@ -7501,14 +7414,6 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/dns-packet@5.6.5':
dependencies:
'@types/node': 25.2.3
'@types/elliptic@6.4.18':
dependencies:
'@types/bn.js': 5.2.0
'@types/express-serve-static-core@5.1.1':
dependencies:
'@types/node': 25.2.3
@@ -7580,10 +7485,6 @@ snapshots:
'@types/minimatch@5.1.2': {}
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.2.0
'@types/ms@2.1.0': {}
'@types/mute-stream@0.0.4':
@@ -7829,8 +7730,6 @@ snapshots:
bintrees@1.0.2: {}
bn.js@4.12.2: {}
body-parser@2.2.2:
dependencies:
bytes: 3.1.2
@@ -7867,8 +7766,6 @@ snapshots:
p-queue: 6.6.2
unload: 2.4.1
brorand@1.1.0: {}
bson@6.10.4: {}
bson@7.2.0: {}
@@ -8019,6 +7916,8 @@ snapshots:
crelt@1.0.6: {}
croner@10.0.1: {}
croner@9.1.0: {}
cross-spawn@7.0.6:
@@ -8091,10 +7990,6 @@ snapshots:
devtools-protocol@0.0.1566079: {}
dns-packet@5.6.1:
dependencies:
'@leichtgewicht/ip-codec': 2.0.5
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -8131,16 +8026,6 @@ snapshots:
ee-first@1.1.1: {}
elliptic@6.6.1:
dependencies:
bn.js: 4.12.2
brorand: 1.1.0
hash.js: 1.1.7
hmac-drbg: 1.0.1
inherits: 2.0.4
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -8570,11 +8455,6 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hash.js@1.1.7:
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -8607,12 +8487,6 @@ snapshots:
highlight.js@11.11.1: {}
hmac-drbg@1.0.1:
dependencies:
hash.js: 1.1.7
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
html-minifier@4.0.0:
dependencies:
camel-case: 3.0.0
@@ -9321,10 +9195,6 @@ snapshots:
mingo@7.2.0: {}
minimalistic-assert@1.0.1: {}
minimalistic-crypto-utils@1.0.1: {}
minimatch@10.1.2:
dependencies:
'@isaacs/brace-expansion': 5.0.1

102
readme.md
View File

@@ -21,6 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [Email System](#email-system)
- [DNS Server](#dns-server)
- [RADIUS Server](#radius-server)
- [Certificate Management](#certificate-management)
- [Storage & Caching](#storage--caching)
- [Security Features](#security-features)
- [OpsServer Dashboard](#opsserver-dashboard)
@@ -46,7 +47,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Hierarchical rate limiting** — global, per-domain, per-sender
### 🔒 Enterprise Security
- **Automatic TLS certificates** via ACME with Cloudflare DNS-01 challenges
- **Automatic TLS certificates** via ACME (smartacme v9) with Cloudflare DNS-01 challenges
- **Smart certificate scheduling** — per-domain deduplication, controlled parallelism, and account rate limiting handled automatically
- **Per-domain exponential backoff** — failed provisioning attempts are tracked and backed off to avoid hammering ACME servers
- **IP reputation checking** with caching and configurable thresholds
- **Content scanning** for spam, viruses, and malicious attachments
- **Security event logging** with structured audit trails
@@ -73,7 +76,8 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🖥️ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring
- **JWT authentication** with session persistence
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, and security events
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, and security events
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
- **Read-only configuration display** — DcRouter is configured through code
## Installation
@@ -250,7 +254,7 @@ graph TB
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
DS[SmartDNS Server<br/><i>Rust-powered</i>]
RS[SmartRadius Server]
CM[Certificate Manager]
CM[Certificate Manager<br/><i>smartacme v9</i>]
OS[OpsServer Dashboard]
MM[Metrics Manager]
SM[Storage Manager]
@@ -297,6 +301,7 @@ graph TB
| **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) |
| **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) |
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting |
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
@@ -308,8 +313,8 @@ graph TB
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, and SmartRadius based on which configs are provided.
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery.
3. **On `stop()`**: All services are gracefully shut down in reverse order.
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
### Rust-Powered Architecture
@@ -584,15 +589,6 @@ match: { sizeRange: { min: 1000, max: 5000000 }, hasAttachments: true }
match: { subject: /invoice|receipt/i }
```
### Socket-Handler Mode 🔌
When `useSocketHandler: true` is set, SmartProxy passes sockets directly to the email server — no internal port binding, lower latency, and fewer open ports:
```
Traditional: External Port → SmartProxy → Internal Port → Email Server
Socket Mode: External Port → SmartProxy → (direct socket) → Email Server
```
### Email Security Stack
- **DKIM** — Automatic key generation, signing, and rotation for all domains
@@ -705,6 +701,73 @@ RADIUS is fully manageable at runtime via the OpsServer API:
- Session monitoring and forced disconnects
- Accounting summaries and statistics
## Certificate Management
DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
### How It Works
When a `dnsChallenge` is configured (e.g. with a Cloudflare API key), DcRouter creates a SmartAcme instance that handles DNS-01 challenges for automatic certificate provisioning. SmartProxy calls the `certProvisionFunction` whenever a route needs a TLS certificate, and SmartAcme takes care of the rest.
```typescript
const router = new DcRouter({
smartProxyConfig: {
routes: [
{
name: 'secure-app',
match: { domains: ['app.example.com'], ports: [443] },
action: {
type: 'forward',
targets: [{ host: '192.168.1.10', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' } // ← triggers ACME provisioning
}
}
],
acme: { email: 'admin@example.com', enabled: true, useProduction: true }
},
tls: { contactEmail: 'admin@example.com' },
dnsChallenge: { cloudflareApiKey: process.env.CLOUDFLARE_API_KEY }
});
```
### smartacme v9 Features
| Feature | Description |
|---------|-------------|
| **Per-domain deduplication** | Concurrent requests for the same domain share a single ACME operation |
| **Global concurrency cap** | Default 5 parallel ACME operations to prevent overload |
| **Account rate limiting** | Sliding window (250 orders / 3 hours) to stay within ACME provider limits |
| **Structured errors** | `AcmeError` with `isRetryable`, `isRateLimited`, `retryAfter` fields |
| **Clean shutdown** | `stop()` properly destroys HTTP agents and DNS clients |
### Per-Domain Backoff
DcRouter's `CertProvisionScheduler` adds **per-domain exponential backoff** on top of smartacme's built-in protections. If a DNS-01 challenge fails for a domain:
1. The failure is recorded (persisted to storage)
2. The domain enters backoff: `min(failures² × 1 hour, 24 hours)`
3. Subsequent requests for that domain are rejected until the backoff expires
4. On success, the backoff is cleared
This prevents hammering ACME servers for domains with persistent issues (e.g. missing DNS delegation).
### Fallback to HTTP-01
If DNS-01 fails, the `certProvisionFunction` returns `'http01'` to tell SmartProxy to fall back to HTTP-01 challenge validation. This provides a safety net for domains where DNS-01 isn't viable.
### Certificate Storage
Certificates are persisted via the `StorageBackedCertManager` which uses DcRouter's `StorageManager`. This means certs survive restarts and don't need to be re-provisioned unless they expire.
### Dashboard
The OpsServer includes a **Certificates** view showing:
- All domains with their certificate status (valid, expiring, expired, failed)
- Certificate source (ACME, provision function, static)
- Expiry dates and issuer information
- Backoff status for failed domains
- One-click reprovisioning per domain
## Storage & Caching
### StorageManager
@@ -725,7 +788,7 @@ storage: {
// Simply omit the storage config
```
Used for: DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs.
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state.
### Cache Database
@@ -811,6 +874,7 @@ The OpsServer provides a web-based management interface served on port 3000. It'
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning |
| 📜 **Logs** | Real-time log viewer with level filtering and search |
| ⚙️ **Configuration** | Read-only view of current system configuration |
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
@@ -838,6 +902,11 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'getBounceRecords' // Bounce records
'removeFromSuppressionList' // Unsuppress an address
// Certificates
'getCertificateOverview' // Domain-centric certificate status
'reprovisionCertificate' // Reprovision by route name (legacy)
'reprovisionCertificateDomain' // Reprovision by domain (preferred)
// Configuration (read-only)
'getConfiguration' // Current system config
@@ -884,6 +953,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|----------|------|-------------|
| `options` | `IDcRouterOptions` | Current configuration |
| `smartProxy` | `SmartProxy` | SmartProxy instance |
| `smartAcme` | `SmartAcme` | SmartAcme v9 certificate manager instance |
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
| `dnsServer` | `DnsServer` | DNS server instance |
| `radiusServer` | `RadiusServer` | RADIUS server instance |
@@ -891,6 +961,8 @@ const router = new DcRouter(options: IDcRouterOptions);
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance |
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
### Re-exported Types

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '5.0.6',
version: '6.2.1',
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 storage manager
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 { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
@@ -171,6 +173,7 @@ export class DcRouter {
// Core services
public smartProxy?: plugins.smartproxy.SmartProxy;
public smartAcme?: plugins.smartacme.SmartAcme;
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
public emailServer?: UnifiedEmailServer;
public radiusServer?: RadiusServer;
@@ -182,6 +185,19 @@ export class DcRouter {
public cacheDb?: CacheDb;
public cacheCleaner?: CacheCleaner;
// Certificate status tracking from SmartProxy events (keyed by domain)
public certificateStatusMap = new Map<string, {
status: 'valid' | 'failed';
routeNames: string[];
expiryDate?: string;
issuedAt?: string;
source?: string;
error?: string;
}>();
// Certificate provisioning scheduler with per-domain backoff
public certProvisionScheduler?: CertProvisionScheduler;
// TypedRouter for API endpoints
public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -194,6 +210,13 @@ export class DcRouter {
...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
this.storageManager = new StorageManager(this.options.storage);
}
@@ -426,15 +449,83 @@ export class DcRouter {
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
...this.options.smartProxyConfig,
routes,
acme: acmeConfig
acme: acmeConfig,
certStore: {
loadAll: async () => {
const keys = await this.storageManager.list('/proxy-certs/');
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
for (const key of keys) {
const data = await this.storageManager.getJSON(key);
if (data) certs.push(data);
}
return certs;
},
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
domain, publicKey, privateKey, ca,
});
},
remove: async (domain: string) => {
await this.storageManager.delete(`/proxy-certs/${domain}`);
},
},
};
// If we have DNS challenge handlers, enhance the config
// Initialize cert provision scheduler
this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager);
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
if (challengeHandlers.length > 0) {
// We'll need to pass this to SmartProxy somehow
// For now, we'll set it as a property
(smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers;
(smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01'];
this.smartAcme = new plugins.smartacme.SmartAcme({
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
certManager: new StorageBackedCertManager(this.storageManager),
environment: 'production',
challengeHandlers: challengeHandlers,
challengePriority: ['dns-01'],
});
await this.smartAcme.start();
const scheduler = this.certProvisionScheduler;
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 {
// smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
eventComms.setSource('smartacme-dns-01');
const isWildcardDomain = domain.startsWith('*.');
const cert = await this.smartAcme.getCertificateForDomain(domain, {
includeWildcard: !isWildcardDomain,
});
if (cert.validUntil) {
eventComms.setExpiryDate(new Date(cert.validUntil));
}
const result = {
id: cert.id,
domainName: cert.domainName,
created: cert.created,
validUntil: cert.validUntil,
privateKey: cert.privateKey,
publicKey: cert.publicKey,
csr: cert.csr,
};
// Success — clear any backoff
await scheduler.clearBackoff(domain);
return result;
} 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`);
return 'http01';
}
};
}
// Create SmartProxy instance
@@ -453,19 +544,36 @@ export class DcRouter {
console.error('[DcRouter] Error stack:', err.stack);
});
if (acmeConfig) {
this.smartProxy.on('certificate-issued', (event) => {
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
// 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) => {
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, {
status: 'valid', routeNames,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source,
});
});
this.smartProxy.on('certificate-renewed', (event) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, {
status: 'valid', routeNames,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source,
});
});
this.smartProxy.on('certificate-failed', (event) => {
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, {
status: 'failed', routeNames, error: event.error,
source: event.source,
});
});
}
// Start SmartProxy
console.log('[DcRouter] Starting SmartProxy...');
@@ -614,27 +722,66 @@ export class DcRouter {
* @returns Whether the domain matches the pattern
*/
private isDomainMatch(domain: string, pattern: string): boolean {
// Normalize inputs
domain = domain.toLowerCase();
pattern = pattern.toLowerCase();
// Check for exact match
if (domain === pattern) {
return true;
if (domain === pattern) return true;
// Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
}
// Check for wildcard match (*.example.com)
// Standard wildcard: *.example.com matches sub.example.com and example.com
if (pattern.startsWith('*.')) {
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
// Check if domain ends with the pattern suffix and has at least one character before it
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
const suffix = pattern.slice(2);
if (domain === suffix) return true;
return domain.endsWith(suffix) && domain.length > suffix.length;
}
// No match
return false;
}
/**
* Find the first route name that matches a given domain
*/
private findRouteNameForDomain(domain: string): string | undefined {
if (!this.smartProxy) return undefined;
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)) return route.name;
}
}
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() {
console.log('Stopping DcRouter services...');
@@ -652,6 +799,9 @@ export class DcRouter {
// Stop unified email server if running
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
// Stop SmartAcme if running
this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(),
// Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),

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

@@ -147,8 +147,10 @@ export class MetricsManager {
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
throughput: proxyMetrics ? {
bytesIn: proxyMetrics.totals.bytesIn(),
bytesOut: proxyMetrics.totals.bytesOut()
} : { bytesIn: 0, bytesOut: 0 },
bytesOut: proxyMetrics.totals.bytesOut(),
bytesInPerSecond: proxyMetrics.throughput.instant().in,
bytesOutPerSecond: proxyMetrics.throughput.instant().out,
} : { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
};
});
}
@@ -487,8 +489,12 @@ export class MetricsManager {
return {
connectionsByIP: new Map<string, number>(),
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
topIPs: [],
topIPs: [] as Array<{ ip: string; count: number }>,
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
throughputByIP: new Map<string, { in: number; out: number }>(),
requestsPerSecond: 0,
requestsTotal: 0,
};
}
@@ -511,11 +517,25 @@ export class MetricsManager {
bytesOut: proxyMetrics.totals.bytesOut()
};
// Get throughput history from Rust engine (up to 300 seconds)
const throughputHistory = proxyMetrics.throughput.history(300);
// Get per-IP throughput
const throughputByIP = proxyMetrics.throughput.byIP();
// Get HTTP request rates
const requestsPerSecond = proxyMetrics.requests.perSecond();
const requestsTotal = proxyMetrics.requests.total();
return {
connectionsByIP,
throughputRate,
topIPs,
totalDataTransferred,
throughputHistory,
throughputByIP,
requestsPerSecond,
requestsTotal,
};
}, 200); // Use 200ms cache for more frequent updates
}

View File

@@ -18,6 +18,7 @@ export class OpsServer {
private statsHandler: handlers.StatsHandler;
private radiusHandler: handlers.RadiusHandler;
private emailOpsHandler: handlers.EmailOpsHandler;
private certificateHandler: handlers.CertificateHandler;
constructor(dcRouterRefArg: DcRouter) {
this.dcRouterRef = dcRouterRefArg;
@@ -57,6 +58,7 @@ export class OpsServer {
this.statsHandler = new handlers.StatsHandler(this);
this.radiusHandler = new handlers.RadiusHandler(this);
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
this.certificateHandler = new handlers.CertificateHandler(this);
console.log('✅ OpsServer TypedRequest handlers initialized');
}

View File

@@ -0,0 +1,311 @@
import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
export class CertificateHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Certificate Overview
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
'getCertificateOverview',
async (dataArg) => {
const certificates = await this.buildCertificateOverview();
const summary = this.buildSummary(certificates);
return { certificates, summary };
}
)
);
// Legacy route-based reprovision (backward compat)
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
'reprovisionCertificate',
async (dataArg) => {
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[]> {
const dcRouter = this.opsServerRef.dcRouterRef;
const smartProxy = dcRouter.smartProxy;
if (!smartProxy) return [];
const routes = smartProxy.routeManager.getRoutes();
// 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) {
if (!route.name) continue;
const tls = route.action?.tls;
if (!tls) continue;
// Skip passthrough routes - they don't manage certificates
if (tls.mode === 'passthrough') continue;
const routeDomains = route.match.domains
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
: [];
// Determine source
let source: interfaces.requests.TCertificateSource = 'none';
if (tls.certificate === 'auto') {
if ((smartProxy.settings as any).certProvisionFunction) {
source = 'provision-function';
} else {
source = 'acme';
}
} else if (tls.certificate && typeof tls.certificate === 'object') {
source = 'static';
}
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 expiryDate: string | undefined;
let issuedAt: string | undefined;
let issuer: string | undefined;
let error: string | undefined;
// Check event-based status from certificateStatusMap (now keyed by domain)
const eventStatus = dcRouter.certificateStatusMap.get(domain);
if (eventStatus) {
status = eventStatus.status;
expiryDate = eventStatus.expiryDate;
issuedAt = eventStatus.issuedAt;
error = eventStatus.error;
if (eventStatus.source) {
issuer = eventStatus.source;
}
}
// Try SmartProxy certificate status if no event data
if (status === 'unknown' && info.routeNames.length > 0) {
try {
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
if (rustStatus) {
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
if (rustStatus.issuer) issuer = rustStatus.issuer;
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
status = rustStatus.status;
}
}
} catch {
// Rust bridge may not support this command yet — ignore
}
}
// 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')) {
const expiry = new Date(expiryDate);
const now = new Date();
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry < 0) {
status = 'expired';
} else if (daysUntilExpiry < 30) {
status = 'expiring';
} else {
status = 'valid';
}
}
// Static certs with no other info default to 'valid'
if (info.source === 'static' && status === 'unknown') {
status = 'valid';
}
// 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({
domain,
routeNames: info.routeNames,
status,
source: info.source,
tlsMode: info.tlsMode,
expiryDate,
issuer,
issuedAt,
error,
canReprovision: info.canReprovision,
backoffInfo,
});
}
return certificates;
}
private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
total: number;
valid: number;
expiring: number;
expired: number;
failed: number;
unknown: number;
} {
const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
summary.total = certificates.length;
for (const cert of certificates) {
switch (cert.status) {
case 'valid': summary.valid++; break;
case 'expiring': summary.expiring++; break;
case 'expired': summary.expired++; break;
case 'failed': summary.failed++; break;
case 'provisioning': // count as unknown
case 'unknown': summary.unknown++; break;
}
}
return summary;
}
/**
* Legacy route-based reprovisioning
*/
private async reprovisionCertificateByRoute(routeName: 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' };
}
try {
await smartProxy.provisionCertificate(routeName);
// Clear event-based status for domains in this route
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}'` };
} catch (err) {
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

@@ -5,3 +5,4 @@ export * from './security.handler.js';
export * from './stats.handler.js';
export * from './radius.handler.js';
export * from './email-ops.handler.js';
export * from './certificate.handler.js';

View File

@@ -85,11 +85,23 @@ export class SecurityHandler {
if (this.opsServerRef.dcRouterRef.metricsManager) {
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
// Convert per-IP throughput Map to serializable array
const throughputByIP: Array<{ ip: string; in: number; out: number }> = [];
if (networkStats.throughputByIP) {
for (const [ip, tp] of networkStats.throughputByIP) {
throughputByIP.push({ ip, in: tp.in, out: tp.out });
}
}
return {
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
throughputRate: networkStats.throughputRate,
topIPs: networkStats.topIPs,
totalDataTransferred: networkStats.totalDataTransferred,
throughputHistory: networkStats.throughputHistory || [],
throughputByIP,
requestsPerSecond: networkStats.requestsPerSecond || 0,
requestsTotal: networkStats.requestsTotal || 0,
};
}
@@ -99,6 +111,10 @@ export class SecurityHandler {
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
topIPs: [],
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
throughputHistory: [],
throughputByIP: [],
requestsPerSecond: 0,
requestsTotal: 0,
};
}
)

View File

@@ -27,6 +27,8 @@ export class StatsHandler {
cpuUsage: stats.cpuUsage,
activeConnections: stats.activeConnections,
totalConnections: stats.totalConnections,
requestsPerSecond: stats.requestsPerSecond,
throughput: stats.throughput,
},
history: dataArg.includeHistory ? stats.history : undefined,
};
@@ -191,6 +193,8 @@ export class StatsHandler {
cpuUsage: stats.cpuUsage,
activeConnections: stats.activeConnections,
totalConnections: stats.totalConnections,
requestsPerSecond: stats.requestsPerSecond,
throughput: stats.throughput,
};
})
);
@@ -247,36 +251,39 @@ export class StatsHandler {
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
promises.push(
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
stats.connectionsByIP.forEach((count, ip) => {
connectionDetails.push({
remoteAddress: ip,
protocol: 'https' as any,
state: 'established' as any,
startTime: Date.now(),
bytesIn: 0,
bytesOut: 0,
});
});
(async () => {
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
const serverStats = await this.collectServerStats();
// Build per-IP bandwidth lookup from throughputByIP
const ipBandwidth = new Map<string, { in: number; out: number }>();
if (stats.throughputByIP) {
for (const [ip, tp] of stats.throughputByIP) {
ipBandwidth.set(ip, { in: tp.in, out: tp.out });
}
}
metrics.network = {
totalBandwidth: {
in: stats.throughputRate.bytesInPerSecond,
out: stats.throughputRate.bytesOutPerSecond,
},
activeConnections: stats.connectionsByIP.size,
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
totalBytes: {
in: stats.totalDataTransferred.bytesIn,
out: stats.totalDataTransferred.bytesOut,
},
activeConnections: serverStats.activeConnections,
connectionDetails: [],
topEndpoints: stats.topIPs.map(ip => ({
endpoint: ip.ip,
requests: ip.count,
bandwidth: {
in: 0,
out: 0,
},
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
})),
throughputHistory: stats.throughputHistory || [],
requestsPerSecond: stats.requestsPerSecond || 0,
requestsTotal: stats.requestsTotal || 0,
};
})
})()
);
}
@@ -301,6 +308,7 @@ export class StatsHandler {
requestsPerSecond: number;
activeConnections: number;
totalConnections: number;
throughput: interfaces.data.IServerStats['throughput'];
history: Array<{
timestamp: number;
value: number;
@@ -316,6 +324,7 @@ export class StatsHandler {
requestsPerSecond: serverStats.requestsPerSecond,
activeConnections: serverStats.activeConnections,
totalConnections: serverStats.totalConnections,
throughput: serverStats.throughput,
history: [], // TODO: Implement history tracking
};
}
@@ -340,6 +349,7 @@ export class StatsHandler {
requestsPerSecond: 0,
activeConnections: 0,
totalConnections: 0,
throughput: { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
history: [],
};
}

View File

@@ -378,7 +378,7 @@ export class StorageManager {
*/
async getJSON<T = any>(key: string): Promise<T | null> {
const value = await this.get(key);
if (value === null) {
if (value === null || value.trim() === '') {
return null;
}

View File

@@ -17,6 +17,13 @@ export interface IServerStats {
};
activeConnections: number;
totalConnections: number;
requestsPerSecond: number;
throughput: {
bytesIn: number;
bytesOut: number;
bytesInPerSecond: number;
bytesOutPerSecond: number;
};
}
export interface IEmailStats {
@@ -109,6 +116,10 @@ export interface INetworkMetrics {
in: number;
out: number;
};
totalBytes?: {
in: number;
out: number;
};
activeConnections: number;
connectionDetails: IConnectionDetails[];
topEndpoints: Array<{
@@ -119,6 +130,9 @@ export interface INetworkMetrics {
out: number;
};
}>;
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
requestsPerSecond?: number;
requestsTotal?: number;
}
export interface IConnectionDetails {

View File

@@ -128,6 +128,37 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
#### 🔐 Certificates
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status |
| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) |
| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) |
#### Certificate Types
```typescript
type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown';
type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
interface ICertificateInfo {
domain: string;
routeNames: string[];
status: TCertificateStatus;
source: TCertificateSource;
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
expiryDate?: string;
issuer?: string;
issuedAt?: string;
error?: string;
canReprovision: boolean;
backoffInfo?: {
failures: number;
retryAfter?: string;
lastError?: string;
};
}
```
#### 📡 RADIUS
| Interface | Method | Description |
|-----------|--------|-------------|
@@ -173,7 +204,16 @@ console.log('Email:', metrics.emailStats);
console.log('DNS:', metrics.dnsStats);
console.log('Security:', metrics.securityMetrics);
// 3. Check email queues
// 3. Check certificate status
const certClient = new typedrequest.TypedRequest<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>(
'https://your-dcrouter:3000/typedrequest',
'getQueuedEmails'

View File

@@ -0,0 +1,76 @@
import * as plugins from '../plugins.js';
import * as authInterfaces from '../data/auth.js';
export type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown';
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
export interface ICertificateInfo {
domain: string;
routeNames: string[];
status: TCertificateStatus;
source: TCertificateSource;
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
expiryDate?: string; // ISO string
issuer?: string;
issuedAt?: string; // ISO string
error?: string; // if status === 'failed'
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<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetCertificateOverview
> {
method: 'getCertificateOverview';
request: {
identity?: authInterfaces.IIdentity;
};
response: {
certificates: ICertificateInfo[];
summary: {
total: number;
valid: number;
expiring: number;
expired: number;
failed: number;
unknown: number;
};
};
}
// Legacy route-based reprovision (kept for backward compat)
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ReprovisionCertificate
> {
method: 'reprovisionCertificate';
request: {
identity?: authInterfaces.IIdentity;
routeName: string;
};
response: {
success: boolean;
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

@@ -5,3 +5,4 @@ export * from './stats.js';
export * from './combined.stats.js';
export * from './radius.js';
export * from './email-ops.js';
export * from './certificate.js';

View File

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

View File

@@ -47,12 +47,25 @@ export interface INetworkState {
connections: interfaces.data.IConnectionInfo[];
connectionsByIP: { [ip: string]: number };
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
totalBytes: { in: number; out: number };
topIPs: Array<{ ip: string; count: number }>;
throughputByIP: Array<{ ip: string; in: number; out: number }>;
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
requestsPerSecond: number;
requestsTotal: number;
lastUpdated: number;
isLoading: boolean;
error: string | null;
}
export interface ICertificateState {
certificates: interfaces.requests.ICertificateInfo[];
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
isLoading: boolean;
error: string | null;
lastUpdated: number;
}
export interface IEmailOpsState {
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
queuedEmails: interfaces.requests.IEmailQueueItem[];
@@ -103,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
// Determine initial view from URL path
const getInitialView = (): string => {
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'];
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
const segments = path.split('/').filter(Boolean);
const view = segments[0];
return validViews.includes(view) ? view : 'overview';
@@ -136,7 +149,12 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
connections: [],
connectionsByIP: {},
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
totalBytes: { in: 0, out: 0 },
topIPs: [],
throughputByIP: [],
throughputHistory: [],
requestsPerSecond: 0,
requestsTotal: 0,
lastUpdated: 0,
isLoading: false,
error: null,
@@ -162,6 +180,18 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
'soft'
);
export const certificateStatePart = await appState.getStatePart<ICertificateState>(
'certificates',
{
certificates: [],
summary: { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 },
isLoading: false,
error: null,
lastUpdated: 0,
},
'soft'
);
// Actions for state management
interface IActionContext {
identity: interfaces.data.IIdentity | null;
@@ -341,6 +371,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
}, 100);
}
// If switching to certificates view, ensure we fetch certificate data
if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
setTimeout(() => {
certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
}, 100);
}
return {
...currentState,
activeView: viewName,
@@ -394,7 +431,14 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
connections: connectionsResponse.connections,
connectionsByIP,
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
totalBytes: networkStatsResponse.totalDataTransferred
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
: { in: 0, out: 0 },
topIPs: networkStatsResponse.topIPs || [],
throughputByIP: networkStatsResponse.throughputByIP || [],
throughputHistory: networkStatsResponse.throughputHistory || [],
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
requestsTotal: networkStatsResponse.requestsTotal || 0,
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -641,6 +685,66 @@ export const removeFromSuppressionListAction = emailOpsStatePart.createAction<st
}
);
// ============================================================================
// Certificate Actions
// ============================================================================
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCertificateOverview
>('/typedrequest', 'getCertificateOverview');
const response = await request.fire({
identity: context.identity,
});
return {
certificates: response.certificates,
summary: response.summary,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
};
}
});
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_ReprovisionCertificateDomain
>('/typedrequest', 'reprovisionCertificateDomain');
await request.fire({
identity: context.identity,
domain,
});
// Re-fetch overview after reprovisioning
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
return statePartArg.getState();
} catch (error) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
};
}
}
);
// Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() {
const context = getActionContext();
@@ -703,7 +807,12 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
throughputHistory: network.throughputHistory || [],
requestsPerSecond: network.requestsPerSecond || 0,
requestsTotal: network.requestsTotal || 0,
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -718,13 +827,27 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out
},
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
throughputHistory: network.throughputHistory || [],
requestsPerSecond: network.requestsPerSecond || 0,
requestsTotal: network.requestsTotal || 0,
lastUpdated: Date.now(),
isLoading: false,
error: null,
});
}
}
// Refresh certificate data if on certificates view
if (currentView === 'certificates') {
try {
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
} catch (error) {
console.error('Certificate refresh failed:', error);
}
}
} catch (error) {
console.error('Combined refresh failed:', error);
}
@@ -749,13 +872,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency
dispatchCombinedRefreshAction();
// If network view is active, also ensure we have fresh network data
const currentView = uiStatePart.getState().activeView;
if (currentView === 'network') {
// Network view needs more frequent updates, fetch directly
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
}
}, uiState.refreshInterval);
}
} else {

View File

@@ -5,4 +5,5 @@ export * from './ops-view-emails.js';
export * from './ops-view-logs.js';
export * from './ops-view-config.js';
export * from './ops-view-security.js';
export * from './ops-view-certificates.js';
export * from './shared/index.js';

View File

@@ -19,6 +19,7 @@ import { OpsViewEmails } from './ops-view-emails.js';
import { OpsViewLogs } from './ops-view-logs.js';
import { OpsViewConfig } from './ops-view-config.js';
import { OpsViewSecurity } from './ops-view-security.js';
import { OpsViewCertificates } from './ops-view-certificates.js';
@customElement('ops-dashboard')
export class OpsDashboard extends DeesElement {
@@ -61,6 +62,10 @@ export class OpsDashboard extends DeesElement {
name: 'Security',
element: OpsViewSecurity,
},
{
name: 'Certificates',
element: OpsViewCertificates,
},
];
/**

View File

@@ -0,0 +1,380 @@
import {
DeesElement,
html,
customElement,
type TemplateResult,
css,
state,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { viewHostCss } from './shared/css.js';
import { type IStatsTile } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
'ops-view-certificates': OpsViewCertificates;
}
}
@customElement('ops-view-certificates')
export class OpsViewCertificates extends DeesElement {
@state()
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState();
constructor() {
super();
const sub = appstate.certificateStatePart.state.subscribe((newState) => {
this.certState = newState;
});
this.rxSubscriptions.push(sub);
}
async connectedCallback() {
await super.connectedCallback();
await appstate.certificateStatePart.dispatchAction(appstate.fetchCertificateOverviewAction, null);
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
.certificatesContainer {
display: flex;
flex-direction: column;
gap: 24px;
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.statusBadge.valid {
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
color: ${cssManager.bdTheme('#166534', '#4ade80')};
}
.statusBadge.expiring {
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
}
.statusBadge.expired,
.statusBadge.failed {
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
}
.statusBadge.provisioning {
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
}
.statusBadge.unknown {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
}
.sourceBadge {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
}
.routePills {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.routePill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
}
.moreCount {
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
padding: 2px 6px;
}
.errorText {
font-size: 12px;
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
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 {
font-size: 12px;
}
.expiryInfo .daysLeft {
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
.expiryInfo .daysLeft.warn {
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
}
.expiryInfo .daysLeft.danger {
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
}
`,
];
public render(): TemplateResult {
const { summary } = this.certState;
return html`
<ops-sectionheading>Certificates</ops-sectionheading>
<div class="certificatesContainer">
${this.renderStatsTiles(summary)}
${this.renderCertificateTable()}
</div>
`;
}
private renderStatsTiles(summary: appstate.ICertificateState['summary']): TemplateResult {
const tiles: IStatsTile[] = [
{
id: 'total',
title: 'Total Certificates',
value: summary.total,
type: 'number',
icon: 'shieldHalved',
color: '#3b82f6',
},
{
id: 'valid',
title: 'Valid',
value: summary.valid,
type: 'number',
icon: 'check',
color: '#22c55e',
},
{
id: 'expiring',
title: 'Expiring Soon',
value: summary.expiring,
type: 'number',
icon: 'clock',
color: '#f59e0b',
},
{
id: 'problems',
title: 'Failed / Expired',
value: summary.failed + summary.expired,
type: 'number',
icon: 'triangleExclamation',
color: '#ef4444',
},
];
return html`
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${200}
.gridActions=${[
{
name: 'Refresh',
iconName: 'arrowsRotate',
action: async () => {
await appstate.certificateStatePart.dispatchAction(
appstate.fetchCertificateOverviewAction,
null
);
},
},
]}
></dees-statsgrid>
`;
}
private renderCertificateTable(): TemplateResult {
return html`
<dees-table
.data=${this.certState.certificates}
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
Domain: cert.domain,
Routes: this.renderRoutePills(cert.routeNames),
Status: this.renderStatusBadge(cert.status),
Source: this.renderSourceBadge(cert.source),
Expires: this.renderExpiry(cert.expiryDate),
Error: cert.backoffInfo
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
: cert.error
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
: '',
})}
.dataActions=${[
{
name: 'Reprovision',
iconName: 'arrowsRotate',
type: ['inRow'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item;
if (!cert.canReprovision) {
const { DeesToast } = await import('@design.estate/dees-catalog');
DeesToast.show({
message: 'This certificate source does not support reprovisioning.',
type: 'warning',
duration: 3000,
});
return;
}
await appstate.certificateStatePart.dispatchAction(
appstate.reprovisionCertificateAction,
cert.domain,
);
const { DeesToast } = await import('@design.estate/dees-catalog');
DeesToast.show({
message: `Reprovisioning triggered for ${cert.domain}`,
type: 'success',
duration: 3000,
});
},
},
{
name: 'View Details',
iconName: 'magnifyingGlass',
type: ['doubleClick', 'contextmenu'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item;
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: `Certificate: ${cert.domain}`,
content: html`
<div style="padding: 20px;">
<dees-dataview-codebox
.heading=${'Certificate Details'}
progLang="json"
.codeToDisplay=${JSON.stringify(cert, null, 2)}
></dees-dataview-codebox>
</div>
`,
menuOptions: [
{
name: 'Copy Domain',
iconName: 'copy',
action: async () => {
await navigator.clipboard.writeText(cert.domain);
},
},
],
});
},
},
]}
heading1="Certificate Status"
heading2="TLS certificates by domain"
searchable
.pagination=${true}
.paginationSize=${50}
dataName="certificate"
></dees-table>
`;
}
private renderRoutePills(routeNames: string[]): TemplateResult {
const maxShow = 3;
const visible = routeNames.slice(0, maxShow);
const remaining = routeNames.length - maxShow;
return html`
<span class="routePills">
${visible.map((r) => html`<span class="routePill">${r}</span>`)}
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
</span>
`;
}
private renderStatusBadge(status: interfaces.requests.TCertificateStatus): TemplateResult {
return html`<span class="statusBadge ${status}">${status}</span>`;
}
private renderSourceBadge(source: interfaces.requests.TCertificateSource): TemplateResult {
const labels: Record<string, string> = {
acme: 'ACME',
'provision-function': 'Custom',
static: 'Static',
none: 'None',
};
return html`<span class="sourceBadge">${labels[source] || source}</span>`;
}
private renderExpiry(expiryDate?: string): TemplateResult {
if (!expiryDate) {
return html`<span style="color: ${cssManager.bdTheme('#9ca3af', '#4b5563')}">--</span>`;
}
const expiry = new Date(expiryDate);
const now = new Date();
const daysLeft = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const dateStr = expiry.toLocaleDateString();
let daysClass = '';
let daysText = '';
if (daysLeft < 0) {
daysClass = 'danger';
daysText = `(expired)`;
} else if (daysLeft < 30) {
daysClass = 'warn';
daysText = `(${daysLeft}d left)`;
} else {
daysText = `(${daysLeft}d left)`;
}
return html`
<span class="expiryInfo">
${dateStr} <span class="daysLeft ${daysClass}">${daysText}</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

@@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement {
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
private trafficUpdateTimer: any = null;
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
// Removed byte tracking - now using real-time data from SmartProxy
private historyLoaded = false; // Whether server-side throughput history has been loaded
constructor() {
super();
@@ -111,6 +110,54 @@ export class OpsViewNetwork extends DeesElement {
this.lastTrafficUpdateTime = now;
}
/**
* Load server-side throughput history into the chart.
* Called once when history data first arrives from the Rust engine.
* This pre-populates the chart so users see historical data immediately
* instead of starting from all zeros.
*/
private loadThroughputHistory() {
const history = this.networkState.throughputHistory;
if (!history || history.length === 0) return;
this.historyLoaded = true;
// Convert history points to chart data format (bytes/sec → Mbit/s)
const historyIn = history.map(p => ({
x: new Date(p.timestamp).toISOString(),
y: Math.round((p.in * 8) / 1000000 * 10) / 10,
}));
const historyOut = history.map(p => ({
x: new Date(p.timestamp).toISOString(),
y: Math.round((p.out * 8) / 1000000 * 10) / 10,
}));
// Use history as the chart data, keeping the most recent 60 points (5 min window)
const sliceStart = Math.max(0, historyIn.length - 60);
this.trafficDataIn = historyIn.slice(sliceStart);
this.trafficDataOut = historyOut.slice(sliceStart);
// If fewer than 60 points, pad the front with zeros
if (this.trafficDataIn.length < 60) {
const now = Date.now();
const range = 5 * 60 * 1000;
const bucketSize = range / 60;
const padCount = 60 - this.trafficDataIn.length;
const firstTimestamp = this.trafficDataIn.length > 0
? new Date(this.trafficDataIn[0].x).getTime()
: now;
const padIn = Array.from({ length: padCount }, (_, i) => ({
x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(),
y: 0,
}));
const padOut = padIn.map(p => ({ ...p }));
this.trafficDataIn = [...padIn, ...this.trafficDataIn];
this.trafficDataOut = [...padOut, ...this.trafficDataOut];
}
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
@@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement {
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private calculateRequestsPerSecond(): number {
// Calculate from actual request data in the last minute
const oneMinuteAgo = Date.now() - 60000;
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
const reqPerSec = Math.round(recentRequests.length / 60);
// Track history for trend (keep last 20 values)
this.requestsPerSecHistory.push(reqPerSec);
if (this.requestsPerSecHistory.length > 20) {
this.requestsPerSecHistory.shift();
}
return reqPerSec;
}
private calculateThroughput(): { in: number; out: number } {
// Use real throughput data from network state
return {
@@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement {
}
private renderNetworkStats(): TemplateResult {
const reqPerSec = this.calculateRequestsPerSecond();
// Use server-side requests/sec from SmartProxy's Rust engine
const reqPerSec = this.networkState.requestsPerSecond || 0;
const throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
// Throughput data is now available in the stats tiles
// Use request count history for the requests/sec trend
// Track requests/sec history for the trend sparkline
this.requestsPerSecHistory.push(reqPerSec);
if (this.requestsPerSecHistory.length > 20) {
this.requestsPerSecHistory.shift();
}
const trendData = [...this.requestsPerSecHistory];
// If we don't have enough data, pad with zeros
while (trendData.length < 20) {
trendData.unshift(0);
}
@@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number',
icon: 'plug',
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
actions: [
{
name: 'View Details',
@@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement {
icon: 'chartLine',
color: '#3b82f6',
trendData: trendData,
description: `Average over last minute`,
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
},
{
id: 'throughputIn',
@@ -426,6 +459,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number',
icon: 'download',
color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
},
{
id: 'throughputOut',
@@ -435,6 +469,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number',
icon: 'upload',
color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
},
];
@@ -461,19 +496,32 @@ export class OpsViewNetwork extends DeesElement {
return html``;
}
// Build per-IP bandwidth lookup
const bandwidthByIP = new Map<string, { in: number; out: number }>();
if (this.networkState.throughputByIP) {
for (const entry of this.networkState.throughputByIP) {
bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out });
}
}
// Calculate total connections across all top IPs
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
return html`
<dees-table
.data=${this.networkState.topIPs}
.displayFunction=${(ipData: { ip: string; count: number }) => ({
.displayFunction=${(ipData: { ip: string; count: number }) => {
const bw = bandwidthByIP.get(ipData.ip);
return {
'IP Address': ipData.ip,
'Connections': ipData.count,
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
})}
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
};
}}
heading1="Top Connected IPs"
heading2="IPs with most active connections"
heading2="IPs with most active connections and bandwidth"
.pagination=${false}
dataName="ip"
></dees-table>
@@ -513,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
}
}
// Generate traffic data based on request history
this.updateTrafficData();
// Load server-side throughput history into chart (once)
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
this.loadThroughputHistory();
}
private updateTrafficData() {
// This method is called when network data updates
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
}
private startTrafficUpdateTimer() {

View File

@@ -135,6 +135,20 @@ export class OpsViewOverview extends DeesElement {
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private formatBitsPerSecond(bytesPerSecond: number): string {
const bitsPerSecond = bytesPerSecond * 8;
const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s'];
let size = bitsPerSecond;
let unitIndex = 0;
while (size >= 1000 && unitIndex < units.length - 1) {
size /= 1000;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private renderServerStats(): TemplateResult {
if (!this.statsState.serverStats) return html``;
@@ -162,6 +176,24 @@ export class OpsViewOverview extends DeesElement {
color: '#3b82f6',
description: `Total: ${this.statsState.serverStats.totalConnections}`,
},
{
id: 'throughputIn',
title: 'Throughput In',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
type: 'text',
icon: 'download',
color: '#22c55e',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
},
{
id: 'throughputOut',
title: 'Throughput Out',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
type: 'text',
icon: 'upload',
color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
},
{
id: 'cpu',
title: 'CPU Usage',

View File

@@ -34,6 +34,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Security** — Security incidents from email processing
- 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
- Real-time log streaming
- Filter by log level (error, warning, info, debug)
@@ -77,6 +84,7 @@ ts_web/
├── ops-view-overview.ts # Overview statistics
├── ops-view-network.ts # Network monitoring
├── ops-view-emails.ts # Email queue management
├── ops-view-certificates.ts # Certificate overview & reprovisioning
├── ops-view-logs.ts # Log viewer
├── ops-view-config.ts # Configuration display
├── ops-view-security.ts # Security dashboard
@@ -132,6 +140,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
/emails/sent → Sent emails
/emails/failed → Failed emails
/emails/security → Security incidents
/certificates → Certificate management
/logs → Log viewer
/configuration → System configuration
/security → Security dashboard

View File

@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'] as const;
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'] as const;
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
export type TValidView = typeof validViews[number];