Compare commits

..

37 Commits

Author SHA1 Message Date
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
108a8bb51d v5.0.6
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-12 22:51:55 +00:00
3c5b26d1c1 fix(deps): bump @push.rocks/smartproxy to ^23.1.4 2026-02-12 22:51:55 +00:00
01fbc3db95 v5.0.5
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-12 16:27:28 +00:00
8dd9770339 fix(dcrouter): remove legacy handling of emailConfig.routes that added domain-based routes 2026-02-12 16:27:28 +00:00
77842647fd v5.0.4
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-12 14:20:42 +00:00
a309145829 fix(cache): use user-writable ~/.serve.zone/dcrouter for TsmDB and centralize data path logic 2026-02-12 14:20:42 +00:00
5de8d38b78 v5.0.3
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-12 13:41:32 +00:00
2d6dbc552e fix(packaging): add files whitelist to package.json and remove Playwright-generated screenshots 2026-02-12 13:41:32 +00:00
f0fae866dc v5.0.2
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-12 10:15:26 +00:00
87c039a63f fix(docs): update documentation and packaging configuration: document smartmta/smartdns integrations, adjust API method names, and add release registry info 2026-02-12 10:15:26 +00:00
35 changed files with 1918 additions and 343 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,5 +1,147 @@
# Changelog # Changelog
## 2026-02-15 - 6.0.0 - BREAKING CHANGE(certs)
Introduce domain-centric certificate provisioning with per-domain exponential backoff and a staggered serial scheduler; add domain-based reprovision API and UI backoff display; change certificate overview API to be domain-first and include backoff info; bump related deps.
- Add CertProvisionScheduler: persistent per-domain exponential backoff, retry calculation, and an in-memory serial stagger queue.
- Integrate scheduler with SmartAcme certProvisionFunction: enqueue provisions, clear backoff on success, record failures to drive backoff.
- Switch certificate event tracking to be keyed by domain (certificateStatusMap now keyed by domain) and add findRouteNamesForDomain helper.
- BREAKING: ICertificateInfo shape changed — replaced routeName/domains with domain and routeNames; added optional backoffInfo (failures, retryAfter, lastError).
- Add domain-based reprovision endpoint (reprovisionCertificateDomain) while retaining legacy route-based reprovision for backward compatibility (internal rename to reprovisionCertificateByRoute).
- Web UI updated to domain-centric certificate overview, displays route pills, backoff indicator and retry timing, and uses domain-based reprovision action.
- Dependency bumps: @push.rocks/smartlog -> ^3.1.11, @push.rocks/smartproxy -> ^25.3.1.
## 2026-02-14 - 5.5.0 - feat(certs)
persist ACME certificates in StorageManager, add storage-backed cert manager, default storage to filesystem, and improve certificate status reporting
- Add StorageBackedCertManager to persist SmartAcme certificates under /certs/ via StorageManager
- Default storage to filesystem path (dcrouterHomeDir/storage) when options.storage is not provided
- Wire SmartAcme to use StorageBackedCertManager and provide SmartProxy certStore handlers that load/save/remove certs under /proxy-certs/
- Ops server certificate handler reads persisted cert data to report expiry/issued dates and treats acme/provision-function routes with no cert data as provisioning
- Bump @push.rocks/smartproxy dependency to ^25.3.0
## 2026-02-14 - 5.4.6 - fix(deps)
bump @push.rocks/smartproxy dependency to ^25.2.2
- Updated dependency @push.rocks/smartproxy: ^25.2.0 → ^25.2.2
- Change is a dependency-only patch update, no source code modifications
- Current package version is 5.4.5; recommend a patch release
## 2026-02-14 - 5.4.5 - fix(dcrouter)
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
- package.json: @push.rocks/smartproxy ^23.1.2 → ^23.1.4
- Dependency-only version bump, no source code changes
## 2026-02-12 - 5.0.5 - fix(dcrouter)
remove legacy handling of emailConfig.routes that added domain-based routes
- Removed loop that added domain-based email routes from emailConfig.routes into emailRoutes
- Previously created match.domains by extracting the recipient domain (split on '@') and defaulted forward target port to 25
- Removed creation of TLS passthrough configuration for those forwarded routes
- This prevents duplicate or incorrect domain-based routes being appended during email route construction
## 2026-02-12 - 5.0.4 - fix(cache)
use user-writable ~/.serve.zone/dcrouter for TsmDB and centralize data path logic
- Default TsmDB storage changed from /etc/dcrouter/tsmdb to ~/.serve.zone/dcrouter/tsmdb
- Introduced dcrouterHomeDir, dataDir, and defaultTsmDbPath in ts/paths.ts
- CacheDb now defaults to defaultTsmDbPath when no storagePath is provided
- DcRouter initialization updated to use paths.defaultTsmDbPath; README and readme.hints updated to document the new defaults
- Avoids /etc permission issues and prevents starting a real MongoDB process in tests by using a user-writable default path
## 2026-02-12 - 5.0.3 - fix(packaging)
add files whitelist to package.json and remove Playwright-generated screenshots
- Add a "files" array to package.json to control published package contents (includes ts/, ts_web/, dist/, dist_*/**, dist_ts/, dist_ts_web/, assets/, cli.js, npmextra.json, readme.md).
- Remove multiple .playwright-mcp/*.png screenshot files (clean up Playwright test artifacts and reduce repository noise/size).
## 2026-02-12 - 5.0.2 - fix(docs)
update documentation and packaging configuration: document smartmta/smartdns integrations, adjust API method names, and add release registry info
- README: document SmartDNS as Rust-powered DNS engine and smartmta as TypeScript+Rust MTA; add Rust-powered architecture section and component package table
- README: update Node.js requirement from 18+ to 20+; replace embedded cache DB TsmDb with LocalTsmDb and reduce listed cached document types
- README & ts_interfaces: rename typedrequest API adminLogin -> adminLoginWithUsernameAndPassword and add/clarify several API methods (logout, suppression management, RADIUS client/VLAN helpers)
- README: update test instructions, change test file references and add a test coverage table
- npmextra.json: re-key package configs (@git.zone/cli, @ship.zone/szci), tidy watch array formatting, and add release.registries and accessLevel for publishing
## 2026-02-11 - 5.0.1 - fix(deps/tests) ## 2026-02-11 - 5.0.1 - fix(deps/tests)
bump two dependencies and disable cache in tests bump two dependencies and disable cache in tests

View File

@@ -3,7 +3,11 @@
"watchers": [ "watchers": [
{ {
"name": "dcrouter-dev", "name": "dcrouter-dev",
"watch": ["ts/**/*.ts", "ts_*/**/*.ts", "test_watch/devserver.ts"], "watch": [
"ts/**/*.ts",
"ts_*/**/*.ts",
"test_watch/devserver.ts"
],
"command": "pnpm run build && tsrun test_watch/devserver.ts", "command": "pnpm run build && tsrun test_watch/devserver.ts",
"restart": true, "restart": true,
"debounce": 500, "debounce": 500,
@@ -22,7 +26,7 @@
} }
] ]
}, },
"gitzone": { "@git.zone/cli": {
"projectType": "service", "projectType": "service",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
@@ -53,9 +57,16 @@
"SMTP STARTTLS", "SMTP STARTTLS",
"DNS management" "DNS management"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"dockerRegistryRepoMap": { "dockerRegistryRepoMap": {
"registry.gitlab.com": "code.foss.global/serve.zone/dcrouter" "registry.gitlab.com": "code.foss.global/serve.zone/dcrouter"

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "5.0.1", "version": "6.0.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -38,18 +38,18 @@
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^8.0.0", "@push.rocks/smartacme": "^8.0.0",
"@push.rocks/smartdata": "^7.0.15", "@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/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.1.11",
"@push.rocks/smartmetrics": "^2.0.10", "@push.rocks/smartmetrics": "^2.0.10",
"@push.rocks/smartmongo": "^5.1.0", "@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.2.1", "@push.rocks/smartmta": "^5.2.2",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^23.1.2", "@push.rocks/smartproxy": "^25.3.1",
"@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
@@ -93,5 +93,17 @@
"puppeteer" "puppeteer"
] ]
}, },
"packageManager": "pnpm@10.11.0" "packageManager": "pnpm@10.11.0",
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
]
} }

187
pnpm-lock.yaml generated
View File

@@ -42,8 +42,8 @@ importers:
specifier: ^7.0.15 specifier: ^7.0.15
version: 7.0.15(socks@2.8.7) version: 7.0.15(socks@2.8.7)
'@push.rocks/smartdns': '@push.rocks/smartdns':
specifier: ^7.8.0 specifier: ^7.8.1
version: 7.8.0 version: 7.8.1
'@push.rocks/smartfile': '@push.rocks/smartfile':
specifier: ^13.1.2 specifier: ^13.1.2
version: 13.1.2 version: 13.1.2
@@ -54,8 +54,8 @@ importers:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
'@push.rocks/smartlog': '@push.rocks/smartlog':
specifier: ^3.1.10 specifier: ^3.1.11
version: 3.1.10 version: 3.1.11
'@push.rocks/smartmetrics': '@push.rocks/smartmetrics':
specifier: ^2.0.10 specifier: ^2.0.10
version: 2.0.10 version: 2.0.10
@@ -63,8 +63,8 @@ importers:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(socks@2.8.7) version: 5.1.0(socks@2.8.7)
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.2.1 specifier: ^5.2.2
version: 5.2.1 version: 5.2.2
'@push.rocks/smartnetwork': '@push.rocks/smartnetwork':
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^23.1.2 specifier: ^25.3.1
version: 23.1.2(socks@2.8.7) version: 25.3.1
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -904,8 +904,8 @@ packages:
'@push.rocks/smartdns@6.2.2': '@push.rocks/smartdns@6.2.2':
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==} resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
'@push.rocks/smartdns@7.8.0': '@push.rocks/smartdns@7.8.1':
resolution: {integrity: sha512-5FX74AAgQSqWPZkpTsI/BbUKBQpZKSvs+UdX9IZpwcuPldI+K7D1WeE02mMAGd1Ncd/sYAMor5CTlhnG6L+QhQ==} resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
@@ -970,8 +970,8 @@ packages:
'@push.rocks/smartlog-interfaces@3.0.2': '@push.rocks/smartlog-interfaces@3.0.2':
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==} resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
'@push.rocks/smartlog@3.1.10': '@push.rocks/smartlog@3.1.11':
resolution: {integrity: sha512-5pf5JyzOE2WTCUislNIW4EHePo1a7hiXB+jbil38+N5hW71AEwcPFe6oGxbp5w9ALlz66hV2+E+25R0SsxN+fQ==} resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==}
'@push.rocks/smartmail@2.2.0': '@push.rocks/smartmail@2.2.0':
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==} resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
@@ -1000,8 +1000,8 @@ packages:
'@push.rocks/smartmongo@5.1.0': '@push.rocks/smartmongo@5.1.0':
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==} resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
'@push.rocks/smartmta@5.2.1': '@push.rocks/smartmta@5.2.2':
resolution: {integrity: sha512-ITgu1kIJxWgiU6q3YDxAp1HoMmC8ECJhEAFbDtUDRIBcg8Flvbmgasjnqew67nFcXq2fKYh3rGECloS62MBQgw==} resolution: {integrity: sha512-0xKUi2BMM0HFYIPdNeNJZFitAiJ9CNbLlOJ8TenT+xInp7DKcSQ7ABER1rJKinPtvDjRDSiSqiF2iQR+O7299g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
cpu: [x64, arm64] cpu: [x64, arm64]
os: [darwin, linux, win32] os: [darwin, linux, win32]
@@ -1040,8 +1040,8 @@ packages:
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@23.1.2': '@push.rocks/smartproxy@25.3.1':
resolution: {integrity: sha512-4uOSPp4ymIBLhn0xocmY+6wPWlEBIB//vaOIPM9wTyoyhWdhMSV2J1V7NcXGNAGiZG9OO4zB1yW3pbs/4Wc2NA==} resolution: {integrity: sha512-kGJGpx3KBUz+qWU2L9B2gbZoUbQEG2BFe6ZzK0b68Y32nHoSIMjol14hzc3sRgW1p/loWy+Gj+5j0KuVytKWmA==}
'@push.rocks/smartpuppeteer@2.0.5': '@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1061,8 +1061,8 @@ packages:
'@push.rocks/smartrouter@1.3.3': '@push.rocks/smartrouter@1.3.3':
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==} resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
'@push.rocks/smartrust@1.2.0': '@push.rocks/smartrust@1.2.1':
resolution: {integrity: sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==} resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
'@push.rocks/smartrx@3.0.10': '@push.rocks/smartrx@3.0.10':
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==} resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
@@ -1131,6 +1131,9 @@ packages:
'@push.rocks/webrequest@3.0.37': '@push.rocks/webrequest@3.0.37':
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==} resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
'@push.rocks/webrequest@4.0.1':
resolution: {integrity: sha512-I60XZZLVf8W5I7YdmUVVu4G92teE3rg3/aKaV00BRg8vJ3VXx3wc59Qj4em7zxQ5o0HvL8m1Aezw3RFMDPyVgA==}
'@push.rocks/webrequest@4.0.2': '@push.rocks/webrequest@4.0.2':
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==} resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
@@ -1847,10 +1850,6 @@ packages:
'@types/minimatch@5.1.2': '@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
'@types/minimatch@6.0.0':
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -2041,6 +2040,10 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.2:
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
engines: {node: 20 || >=22}
bare-events@2.8.2: bare-events@2.8.2:
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
peerDependencies: peerDependencies:
@@ -2109,6 +2112,10 @@ packages:
brace-expansion@2.0.2: brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brace-expansion@5.0.2:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22}
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==} resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
@@ -3282,6 +3289,10 @@ packages:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@10.2.0:
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -4232,7 +4243,7 @@ packages:
hasBin: true hasBin: true
wordwrap@1.0.0: wordwrap@1.0.0:
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=} resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
@@ -4367,7 +4378,7 @@ snapshots:
'@push.rocks/smartfeed': 1.4.0 '@push.rocks/smartfeed': 1.4.0
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2 '@push.rocks/smartmanifest': 2.0.2
@@ -4416,7 +4427,7 @@ snapshots:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2 '@push.rocks/smartmanifest': 2.0.2
@@ -4483,7 +4494,7 @@ snapshots:
'@apiclient.xyz/cloudflare@6.4.3': '@apiclient.xyz/cloudflare@6.4.3':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -4495,7 +4506,7 @@ snapshots:
'@apiclient.xyz/cloudflare@7.1.0': '@apiclient.xyz/cloudflare@7.1.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -5229,7 +5240,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
typescript: 5.9.3 typescript: 5.9.3
@@ -5250,7 +5261,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartinteract': 2.0.16 '@push.rocks/smartinteract': 2.0.16
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -5276,7 +5287,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartnpm': 2.0.6 '@push.rocks/smartnpm': 2.0.6
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
@@ -5311,7 +5322,7 @@ snapshots:
'@push.rocks/smartexpect': 2.5.0 '@push.rocks/smartexpect': 2.5.0
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -5357,7 +5368,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartinteract': 2.0.16 '@push.rocks/smartinteract': 2.0.16
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
'@push.rocks/smartwatch': 6.3.0 '@push.rocks/smartwatch': 6.3.0
@@ -5794,7 +5805,7 @@ snapshots:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5818,7 +5829,7 @@ snapshots:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.6
'@configvault.io/interfaces': 1.0.17 '@configvault.io/interfaces': 1.0.17
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@8.0.0(socks@2.8.7)': '@push.rocks/smartacme@8.0.0(socks@2.8.7)':
@@ -5830,7 +5841,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 6.2.2 '@push.rocks/smartdns': 6.2.2
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
@@ -5942,7 +5953,7 @@ snapshots:
'@push.rocks/smartcli@4.0.20': '@push.rocks/smartcli@4.0.20':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartobject': 1.0.12 '@push.rocks/smartobject': 1.0.12
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5967,7 +5978,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5996,7 +6007,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -6041,18 +6052,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartdns@7.8.0': '@push.rocks/smartdns@7.8.1':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@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/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrust': 1.2.1
'@push.rocks/smartrust': 1.2.0
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@types/dns-packet': 5.6.5
acme-client: 5.4.0 acme-client: 5.4.0
dns-packet: 5.6.1 minimatch: 10.2.0
minimatch: 10.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6207,7 +6215,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 2.0.2 '@api.global/typedrequest-interfaces': 2.0.2
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 4.4.4
'@push.rocks/smartlog@3.1.10': '@push.rocks/smartlog@3.1.11':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3
@@ -6217,12 +6225,12 @@ snapshots:
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 4.0.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@push.rocks/smartmail@2.2.0': '@push.rocks/smartmail@2.2.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.0 '@push.rocks/smartdns': 7.8.1
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartmustache': 3.0.2 '@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6254,7 +6262,7 @@ snapshots:
'@push.rocks/smartmetrics@2.0.10': '@push.rocks/smartmetrics@2.0.10':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@types/pidusage': 2.0.5 '@types/pidusage': 2.0.5
pidtree: 0.6.0 pidtree: 0.6.0
pidusage: 4.0.1 pidusage: 4.0.1
@@ -6323,14 +6331,14 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartmta@5.2.1': '@push.rocks/smartmta@5.2.2':
dependencies: dependencies:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartmail': 2.2.0 '@push.rocks/smartmail': 2.2.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust': 1.2.0 '@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
lru-cache: 11.2.6 lru-cache: 11.2.6
mailparser: 3.9.3 mailparser: 3.9.3
@@ -6344,7 +6352,7 @@ snapshots:
'@push.rocks/smartnetwork@4.4.0': '@push.rocks/smartnetwork@4.4.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.0 '@push.rocks/smartdns': 7.8.1
'@push.rocks/smartping': 1.0.8 '@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -6430,44 +6438,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@23.1.2(socks@2.8.7)': '@push.rocks/smartproxy@25.3.1':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartacme': 8.0.0(socks@2.8.7)
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartrust': 1.2.1
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartrust': 1.2.0
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring': 4.1.0
'@push.rocks/taskbuffer': 4.2.0
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@types/minimatch': 6.0.0 minimatch: 10.2.0
'@types/ws': 8.18.1
minimatch: 10.1.2
pretty-ms: 9.3.0
ws: 8.19.0
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- '@nuxt/kit'
- bare-abort-controller
- bufferutil
- encoding
- gcp-metadata
- kerberos
- mongodb-client-encryption
- react
- react-native-b4a
- snappy
- socks
- supports-color
- utf-8-validate
- vue
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
@@ -6520,7 +6497,7 @@ snapshots:
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
path-to-regexp: 8.3.0 path-to-regexp: 8.3.0
'@push.rocks/smartrust@1.2.0': '@push.rocks/smartrust@1.2.1':
dependencies: dependencies:
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6545,7 +6522,7 @@ snapshots:
'@cfworker/json-schema': 4.1.1 '@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
ws: 8.19.0 ws: 8.19.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -6580,7 +6557,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6684,7 +6661,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6700,7 +6677,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6719,6 +6696,14 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
'@push.rocks/webrequest@4.0.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20
'@push.rocks/webrequest@4.0.2': '@push.rocks/webrequest@4.0.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -7558,10 +7543,6 @@ snapshots:
'@types/minimatch@5.1.2': {} '@types/minimatch@5.1.2': {}
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.1.2
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
@@ -7758,6 +7739,10 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
balanced-match@4.0.2:
dependencies:
jackspeak: 4.2.3
bare-events@2.8.2: {} bare-events@2.8.2: {}
bare-fs@4.5.3: bare-fs@4.5.3:
@@ -7830,6 +7815,10 @@ snapshots:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
brace-expansion@5.0.2:
dependencies:
balanced-match: 4.0.2
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
@@ -9299,6 +9288,10 @@ snapshots:
dependencies: dependencies:
'@isaacs/brace-expansion': 5.0.1 '@isaacs/brace-expansion': 5.0.1
minimatch@10.2.0:
dependencies:
brace-expansion: 5.0.2
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12

View File

@@ -46,7 +46,7 @@ Source at `../../push.rocks/smartmta`, release with `gitzone commit -ypbrt`
### SmartProxy v23.1.2 Route Validation ### SmartProxy v23.1.2 Route Validation
- SmartProxy 23.1.2 enforces stricter route validation - SmartProxy 23.1.2 enforces stricter route validation
- Forward actions MUST use `targets` (array) instead of `target` (singular) - Forward actions MUST use `targets` (array) instead of `target` (singular)
- Test configurations that call `DcRouter.start()` need `cacheConfig: { enabled: false }` to avoid `/etc/dcrouter` permission errors - Test configurations that call `DcRouter.start()` need `cacheConfig: { enabled: false }` to avoid starting a real MongoDB process in tests
```typescript ```typescript
// WRONG - will fail validation // WRONG - will fail validation
@@ -693,7 +693,7 @@ The configuration UI has been converted from an editable interface to a read-onl
## Smartdata Cache System (2026-02-03) ## Smartdata Cache System (2026-02-03)
### Overview ### Overview
DcRouter now uses smartdata + LocalTsmDb for persistent caching. Data is stored at `/etc/dcrouter/tsmdb`. DcRouter now uses smartdata + LocalTsmDb for persistent caching. Data is stored at `~/.serve.zone/dcrouter/tsmdb`.
### Technology Stack ### Technology Stack
| Layer | Package | Purpose | | Layer | Package | Purpose |
@@ -747,7 +747,7 @@ await email.delete();
const dcRouter = new DcRouter({ const dcRouter = new DcRouter({
cacheConfig: { cacheConfig: {
enabled: true, enabled: true,
storagePath: '/etc/dcrouter/tsmdb', storagePath: '~/.serve.zone/dcrouter/tsmdb',
dbName: 'dcrouter', dbName: 'dcrouter',
cleanupIntervalHours: 1, cleanupIntervalHours: 1,
ttlConfig: { ttlConfig: {

191
readme.md
View File

@@ -34,10 +34,10 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🌐 Universal Traffic Router ### 🌐 Universal Traffic Router
- **HTTP/HTTPS routing** with domain matching, path-based forwarding, and automatic TLS - **HTTP/HTTPS routing** with domain matching, path-based forwarding, and automatic TLS
- **TCP/SNI proxy** for any protocol with TLS termination or passthrough - **TCP/SNI proxy** for any protocol with TLS termination or passthrough
- **DNS server** with authoritative zones, dynamic record management, and DNS-over-HTTPS - **DNS server** (Rust-powered via [SmartDNS](https://code.foss.global/push.rocks/smartdns)) with authoritative zones, dynamic record management, and DNS-over-HTTPS
- **Multi-protocol support** on the same infrastructure via [SmartProxy](https://code.foss.global/push.rocks/smartproxy) - **Multi-protocol support** on the same infrastructure via [SmartProxy](https://code.foss.global/push.rocks/smartproxy)
### 📧 Complete Email Infrastructure ### 📧 Complete Email Infrastructure (powered by [smartmta](https://code.foss.global/push.rocks/smartmta))
- **Multi-domain SMTP server** on standard ports (25, 587, 465) - **Multi-domain SMTP server** on standard ports (25, 587, 465)
- **Pattern-based email routing** with four action types: forward, process, deliver, reject - **Pattern-based email routing** with four action types: forward, process, deliver, reject
- **DKIM signing & verification**, SPF, DMARC authentication stack - **DKIM signing & verification**, SPF, DMARC authentication stack
@@ -59,14 +59,16 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### ⚡ High Performance ### ⚡ High Performance
- **Rust-powered proxy engine** via SmartProxy for maximum throughput - **Rust-powered proxy engine** via SmartProxy for maximum throughput
- **Rust-powered MTA engine** via smartmta (TypeScript + Rust hybrid) for reliable email delivery
- **Rust-powered DNS engine** via SmartDNS for high-performance UDP and DNS-over-HTTPS
- **Connection pooling** for outbound SMTP and backend services - **Connection pooling** for outbound SMTP and backend services
- **Socket-handler mode** — direct socket passing eliminates internal port hops - **Socket-handler mode** — direct socket passing eliminates internal port hops
- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput) - **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput)
### 💾 Persistent Storage & Caching ### 💾 Persistent Storage & Caching
- **Multiple storage backends**: filesystem, custom functions, or in-memory - **Multiple storage backends**: filesystem, custom functions, or in-memory
- **Embedded cache database** via smartdata + TsmDb (MongoDB-compatible) - **Embedded cache database** via smartdata + LocalTsmDb (MongoDB-compatible)
- **Automatic TTL-based cleanup** for cached emails, IP reputation, DKIM keys, and more - **Automatic TTL-based cleanup** for cached emails and IP reputation data
### 🖥️ OpsServer Dashboard ### 🖥️ OpsServer Dashboard
- **Web-based management interface** with real-time monitoring - **Web-based management interface** with real-time monitoring
@@ -84,7 +86,7 @@ npm install @serve.zone/dcrouter
### Prerequisites ### Prerequisites
- **Node.js 18+** with ES module support - **Node.js 20+** with ES module support
- Valid domain with DNS control (for ACME certificate automation) - Valid domain with DNS control (for ACME certificate automation)
- Cloudflare API token (for DNS-01 challenges) — optional - Cloudflare API token (for DNS-01 challenges) — optional
@@ -172,7 +174,7 @@ const router = new DcRouter({
acme: { email: 'ssl@example.com', enabled: true, useProduction: true } acme: { email: 'ssl@example.com', enabled: true, useProduction: true }
}, },
// Email system // Email system (powered by smartmta)
emailConfig: { emailConfig: {
ports: [25, 587, 465], ports: [25, 587, 465],
hostname: 'mail.example.com', hostname: 'mail.example.com',
@@ -217,7 +219,7 @@ const router = new DcRouter({
storage: { fsPath: '/var/lib/dcrouter/data' }, storage: { fsPath: '/var/lib/dcrouter/data' },
// Cache database // Cache database
cacheConfig: { enabled: true, storagePath: '/etc/dcrouter/tsmdb' }, cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
// TLS & ACME // TLS & ACME
tls: { contactEmail: 'admin@example.com' }, tls: { contactEmail: 'admin@example.com' },
@@ -244,10 +246,10 @@ graph TB
subgraph "DcRouter Core" subgraph "DcRouter Core"
DC[DcRouter Orchestrator] DC[DcRouter Orchestrator]
SP[SmartProxy Engine] SP[SmartProxy Engine<br/><i>Rust-powered</i>]
ES[Unified Email Server] ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
DS[DNS Server] DS[SmartDNS Server<br/><i>Rust-powered</i>]
RS[RADIUS Server] RS[SmartRadius Server]
CM[Certificate Manager] CM[Certificate Manager]
OS[OpsServer Dashboard] OS[OpsServer Dashboard]
MM[Metrics Manager] MM[Metrics Manager]
@@ -289,17 +291,36 @@ graph TB
### Core Components ### Core Components
| Component | Description | | Component | Package | Description |
|-----------|-------------| |-----------|---------|-------------|
| **DcRouter** | Central orchestrator — starts, stops, and coordinates all services | | **DcRouter** | `@serve.zone/dcrouter` | Central orchestrator — starts, stops, and coordinates all services |
| **SmartProxy** | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config | | **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) |
| **UnifiedEmailServer** | Full SMTP server with pattern-based routing, DKIM, queue management | | **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) |
| **DNS Server** | Authoritative DNS with dynamic records, DKIM TXT auto-generation | | **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
| **RADIUS Server** | Network authentication with MAB, VLAN assignment, and accounting | | **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
| **OpsServer** | Web dashboard + TypedRequest API for monitoring and management | | **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
| **MetricsManager** | Real-time metrics collection (CPU, memory, email, DNS, security) | | **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
| **StorageManager** | Pluggable key-value storage (filesystem, custom, or in-memory) | | **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
| **CacheDb** | Embedded MongoDB-compatible database for persistent caching | | **CacheDb** | `@push.rocks/smartdata` | Embedded MongoDB-compatible database (LocalTsmDb) for persistent caching |
### How It Works
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.
### Rust-Powered Architecture
DcRouter itself is a pure TypeScript orchestrator, but three of its core sub-components ship with **compiled Rust binaries** for performance-critical paths. At runtime each package detects the platform, unpacks the correct binary, and communicates with TypeScript over IPC/FFI — so you get the ergonomics of TypeScript with the throughput of native code.
| Component | Rust Binary | What It Handles |
|-----------|-------------|-----------------|
| **SmartProxy** | `smartproxy-bin` | All TCP/TLS/HTTP proxy networking, NFTables integration, connection metrics |
| **smartmta** | `mailer-bin` | SMTP server + client, DKIM/SPF/DMARC, content scanning, IP reputation |
| **SmartDNS** | `smartdns-bin` | DNS server (UDP + DNS-over-HTTPS), DNSSEC, DNS client resolution |
| **SmartRadius** | — | Pure TypeScript (no Rust component) |
## Configuration Reference ## Configuration Reference
@@ -312,22 +333,8 @@ interface IDcRouterOptions {
smartProxyConfig?: ISmartProxyOptions; smartProxyConfig?: ISmartProxyOptions;
// ── Email ────────────────────────────────────────────────────── // ── Email ──────────────────────────────────────────────────────
/** Unified email server configuration */ /** Unified email server configuration (smartmta) */
emailConfig?: { emailConfig?: IUnifiedEmailServerOptions;
ports: number[]; // e.g. [25, 587, 465]
hostname: string; // e.g. 'mail.example.com'
domains: IEmailDomainConfig[]; // Domain infrastructure
routes: IEmailRoute[]; // Routing rules
useSocketHandler?: boolean; // Direct socket passing (no port binding)
auth?: { required?: boolean; methods?: ('PLAIN'|'LOGIN'|'OAUTH2')[]; users?: Array<{username: string; password: string}> };
tls?: { certPath?: string; keyPath?: string; caPath?: string };
maxMessageSize?: number;
defaults?: {
dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
dkim?: IEmailDomainConfig['dkim'];
rateLimits?: IEmailDomainConfig['rateLimits'];
};
};
/** Custom email port mapping overrides */ /** Custom email port mapping overrides */
emailPortConfig?: { emailPortConfig?: {
@@ -338,9 +345,9 @@ interface IDcRouterOptions {
// ── DNS ──────────────────────────────────────────────────────── // ── DNS ────────────────────────────────────────────────────────
/** Nameserver domains — get A records automatically */ /** Nameserver domains — get A records automatically */
dnsNsDomains?: string[]; // e.g. ['ns1.example.com', 'ns2.example.com'] dnsNsDomains?: string[];
/** Domains this server is authoritative for */ /** Domains this server is authoritative for */
dnsScopes?: string[]; // e.g. ['example.com'] dnsScopes?: string[];
/** Public IP for NS A records */ /** Public IP for NS A records */
publicIp?: string; publicIp?: string;
/** Ingress proxy IPs (hides real server IP) */ /** Ingress proxy IPs (hides real server IP) */
@@ -381,7 +388,7 @@ interface IDcRouterOptions {
}; };
cacheConfig?: { cacheConfig?: {
enabled?: boolean; // default: true enabled?: boolean; // default: true
storagePath?: string; // default: '/etc/dcrouter/tsmdb' storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb'
dbName?: string; // default: 'dcrouter' dbName?: string; // default: 'dcrouter'
cleanupIntervalHours?: number; // default: 1 cleanupIntervalHours?: number; // default: 1
ttlConfig?: { ttlConfig?: {
@@ -453,7 +460,7 @@ DcRouter uses [SmartProxy](https://code.foss.global/push.rocks/smartproxy) for a
## Email System ## Email System
The email system is built around the **UnifiedEmailServer**, which handles SMTP sessions, route matching, delivery queuing, DKIM signing, and all email processing in a single unified component. The email system is powered by [`@push.rocks/smartmta`](https://code.foss.global/push.rocks/smartmta), a TypeScript + Rust hybrid MTA. DcRouter configures and orchestrates smartmta's **UnifiedEmailServer**, which handles SMTP sessions, route matching, delivery queuing, DKIM signing, and all email processing.
### Email Domain Configuration ### Email Domain Configuration
@@ -722,12 +729,12 @@ Used for: DKIM keys, email routes, bounce/suppression lists, IP reputation data,
### Cache Database ### Cache Database
An embedded MongoDB-compatible database (via smartdata + TsmDb) for persistent caching with automatic TTL cleanup: An embedded MongoDB-compatible database (via smartdata + LocalTsmDb) for persistent caching with automatic TTL cleanup:
```typescript ```typescript
cacheConfig: { cacheConfig: {
enabled: true, enabled: true,
storagePath: '/etc/dcrouter/tsmdb', storagePath: '~/.serve.zone/dcrouter/tsmdb',
dbName: 'dcrouter', dbName: 'dcrouter',
cleanupIntervalHours: 1, cleanupIntervalHours: 1,
ttlConfig: { ttlConfig: {
@@ -740,7 +747,7 @@ cacheConfig: {
} }
``` ```
Cached document types: `CachedEmail`, `CachedIPReputation`, `CachedBounce`, `CachedSuppression`, `CachedDKIMKey`. Cached document types: `CachedEmail`, `CachedIPReputation`.
## Security Features ## Security Features
@@ -814,31 +821,39 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
```typescript ```typescript
// Authentication // Authentication
{ method: 'adminLogin', data: { username, password } } 'adminLoginWithUsernameAndPassword' // Login with credentials → returns JWT identity
{ method: 'verifyIdentity', data: { identity } } 'verifyIdentity' // Verify JWT token validity
'adminLogout' // End admin session
// Statistics // Statistics & Health
{ method: 'getServerStatistics', data: { identity } } 'getServerStatistics' // Uptime, CPU, memory, connections
{ method: 'getCombinedMetrics', data: { identity } } 'getHealthStatus' // System health check
{ method: 'getHealthStatus', data: { identity } } 'getCombinedMetrics' // All metrics in one call
// Email Operations // Email Operations
{ method: 'getQueuedEmails', data: { identity } } 'getQueuedEmails' // Emails pending delivery
{ method: 'getSentEmails', data: { identity } } 'getSentEmails' // Successfully delivered emails
{ method: 'getFailedEmails', data: { identity } } 'getFailedEmails' // Failed emails
{ method: 'resendEmail', data: { identity, emailId } } 'resendEmail' // Re-queue a failed email
{ method: 'getBounceRecords', data: { identity } } 'getBounceRecords' // Bounce records
'removeFromSuppressionList' // Unsuppress an address
// Configuration (read-only) // Configuration (read-only)
{ method: 'getConfiguration', data: { identity } } 'getConfiguration' // Current system config
// Logs // Logs
{ method: 'getLogs', data: { identity, level, limit } } 'getLogs' // Retrieve system logs
// RADIUS // RADIUS
{ method: 'getRadiusSessions', data: { identity } } 'getRadiusSessions' // Active RADIUS sessions
{ method: 'getRadiusClients', data: { identity } } 'getRadiusClients' // List NAS clients
{ method: 'getRadiusStatistics', data: { identity } } 'getRadiusStatistics' // RADIUS stats
'setRadiusClient' // Add/update NAS client
'removeRadiusClient' // Remove NAS client
'getVlanMappings' // List VLAN mappings
'setVlanMapping' // Add/update VLAN mapping
'removeVlanMapping' // Remove VLAN mapping
'testVlanAssignment' // Test what VLAN a MAC gets
``` ```
## API Reference ## API Reference
@@ -869,7 +884,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|----------|------|-------------| |----------|------|-------------|
| `options` | `IDcRouterOptions` | Current configuration | | `options` | `IDcRouterOptions` | Current configuration |
| `smartProxy` | `SmartProxy` | SmartProxy instance | | `smartProxy` | `SmartProxy` | SmartProxy instance |
| `emailServer` | `UnifiedEmailServer` | Email server instance | | `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
| `dnsServer` | `DnsServer` | DNS server instance | | `dnsServer` | `DnsServer` | DNS server instance |
| `radiusServer` | `RadiusServer` | RADIUS server instance | | `radiusServer` | `RadiusServer` | RADIUS server instance |
| `storageManager` | `StorageManager` | Storage backend | | `storageManager` | `StorageManager` | Storage backend |
@@ -877,6 +892,21 @@ const router = new DcRouter(options: IDcRouterOptions);
| `metricsManager` | `MetricsManager` | Metrics collector | | `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance | | `cacheDb` | `CacheDb` | Cache database instance |
### Re-exported Types
DcRouter re-exports key types from smartmta for convenience:
```typescript
import {
DcRouter,
IDcRouterOptions,
UnifiedEmailServer,
type IUnifiedEmailServerOptions,
type IEmailRoute,
type IEmailDomainConfig,
} from '@serve.zone/dcrouter';
```
## Sub-Modules ## Sub-Modules
DcRouter is published as a monorepo with separately-installable interface and web packages: DcRouter is published as a monorepo with separately-installable interface and web packages:
@@ -895,31 +925,34 @@ import { data, requests } from '@serve.zone/dcrouter/interfaces';
## Testing ## Testing
DcRouter includes a comprehensive test suite with **198 test files** covering all system components: DcRouter includes a comprehensive test suite covering all system components:
- **SMTP Protocol** — EHLO, MAIL FROM, RCPT TO, DATA, STARTTLS, AUTH, pipelining
- **Email Routing** — Pattern matching, route priorities, all action types
- **Email Security** — DKIM, SPF, DMARC, content scanning, rate limiting
- **DNS** — Record management, socket handler, validation, mode switching
- **RADIUS** — Authentication, VLAN assignment, accounting
- **Deliverability** — IP warmup, reputation monitoring, bounce management
- **Storage & Cache** — All backends, TTL cleanup, persistence
- **OpsServer** — API authentication, protected endpoints, statistics
- **Integration** — Full end-to-end workflows
### Running Tests
```bash ```bash
# Run all tests # Run all tests (10 files, 73 tests)
pnpm test pnpm test
# Run a specific test file # Run a specific test file
tstest test/test.email.router.ts --verbose tstest test/test.jwt-auth.ts --verbose
# Run SMTP protocol suite # Run with extended timeout
tstest test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts --verbose tstest test/test.opsserver-api.ts --verbose --timeout 60
``` ```
### Test Coverage
| Test File | Area | Tests |
|-----------|------|-------|
| `test.contentscanner.ts` | Content scanning (spam, phishing, malware, attachments) | 13 |
| `test.dcrouter.email.ts` | Email config, domain and route setup | 4 |
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
| `test.dns-socket-handler.ts` | DNS socket handler and route generation | 6 |
| `test.errors.ts` | Error classes, handler, retry utilities | 5 |
| `test.ipreputationchecker.ts` | IP reputation, DNSBL, caching, risk classification | 10 |
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 6 |
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.

View File

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

View File

@@ -1,11 +1,12 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { logger } from '../logger.js'; import { logger } from '../logger.js';
import { defaultTsmDbPath } from '../paths.js';
/** /**
* Configuration options for CacheDb * Configuration options for CacheDb
*/ */
export interface ICacheDbOptions { export interface ICacheDbOptions {
/** Base storage path for TsmDB data (default: /etc/dcrouter/tsmdb) */ /** Base storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */
storagePath?: string; storagePath?: string;
/** Database name (default: dcrouter) */ /** Database name (default: dcrouter) */
dbName?: string; dbName?: string;
@@ -29,7 +30,7 @@ export class CacheDb {
constructor(options: ICacheDbOptions = {}) { constructor(options: ICacheDbOptions = {}) {
this.options = { this.options = {
storagePath: options.storagePath || '/etc/dcrouter/tsmdb', storagePath: options.storagePath || defaultTsmDbPath,
dbName: options.dbName || 'dcrouter', dbName: options.dbName || 'dcrouter',
debug: options.debug || false, debug: options.debug || false,
}; };

View File

@@ -0,0 +1,176 @@
import { logger } from './logger.js';
import type { StorageManager } from './storage/index.js';
interface IBackoffEntry {
failures: number;
lastFailure: string; // ISO string
retryAfter: string; // ISO string
lastError?: string;
}
/**
* Manages certificate provisioning scheduling with:
* - Per-domain exponential backoff persisted in StorageManager
* - Serial stagger queue with configurable delay between provisions
*/
export class CertProvisionScheduler {
private storageManager: StorageManager;
private staggerDelayMs: number;
private maxBackoffHours: number;
// In-memory serial queue
private queue: Array<{
domain: string;
fn: () => Promise<any>;
resolve: (value: any) => void;
reject: (err: any) => void;
}> = [];
private processing = false;
// In-memory backoff cache (mirrors storage for fast lookups)
private backoffCache = new Map<string, IBackoffEntry>();
constructor(
storageManager: StorageManager,
options?: { staggerDelayMs?: number; maxBackoffHours?: number }
) {
this.storageManager = storageManager;
this.staggerDelayMs = options?.staggerDelayMs ?? 3000;
this.maxBackoffHours = options?.maxBackoffHours ?? 24;
}
/**
* Storage key for a domain's backoff entry
*/
private backoffKey(domain: string): string {
const clean = domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
return `/cert-backoff/${clean}`;
}
/**
* Load backoff entry from storage (with in-memory cache)
*/
private async loadBackoff(domain: string): Promise<IBackoffEntry | null> {
const cached = this.backoffCache.get(domain);
if (cached) return cached;
const entry = await this.storageManager.getJSON<IBackoffEntry>(this.backoffKey(domain));
if (entry) {
this.backoffCache.set(domain, entry);
}
return entry;
}
/**
* Save backoff entry to both cache and storage
*/
private async saveBackoff(domain: string, entry: IBackoffEntry): Promise<void> {
this.backoffCache.set(domain, entry);
await this.storageManager.setJSON(this.backoffKey(domain), entry);
}
/**
* Check if a domain is currently in backoff
*/
async isInBackoff(domain: string): Promise<boolean> {
const entry = await this.loadBackoff(domain);
if (!entry) return false;
const retryAfter = new Date(entry.retryAfter);
return retryAfter.getTime() > Date.now();
}
/**
* Record a provisioning failure for a domain.
* Sets exponential backoff: min(failures^2 * 1h, maxBackoffHours)
*/
async recordFailure(domain: string, error?: string): Promise<void> {
const existing = await this.loadBackoff(domain);
const failures = (existing?.failures ?? 0) + 1;
// Exponential backoff: failures^2 hours, capped
const backoffHours = Math.min(failures * failures, this.maxBackoffHours);
const retryAfter = new Date(Date.now() + backoffHours * 60 * 60 * 1000);
const entry: IBackoffEntry = {
failures,
lastFailure: new Date().toISOString(),
retryAfter: retryAfter.toISOString(),
lastError: error,
};
await this.saveBackoff(domain, entry);
logger.log('warn', `Cert backoff for ${domain}: ${failures} failures, retry after ${retryAfter.toISOString()}`);
}
/**
* Clear backoff for a domain (on success or manual override)
*/
async clearBackoff(domain: string): Promise<void> {
this.backoffCache.delete(domain);
try {
await this.storageManager.delete(this.backoffKey(domain));
} catch {
// Ignore delete errors (key may not exist)
}
}
/**
* Get backoff info for UI display
*/
async getBackoffInfo(domain: string): Promise<{
failures: number;
retryAfter?: string;
lastError?: string;
} | null> {
const entry = await this.loadBackoff(domain);
if (!entry) return null;
// Only return if still in backoff
const retryAfter = new Date(entry.retryAfter);
if (retryAfter.getTime() <= Date.now()) return null;
return {
failures: entry.failures,
retryAfter: entry.retryAfter,
lastError: entry.lastError,
};
}
/**
* Enqueue a provision operation for serial execution with stagger delay.
* Returns the result of the provision function.
*/
enqueueProvision<T>(domain: string, fn: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.queue.push({ domain, fn, resolve, reject });
this.processQueue();
});
}
/**
* Process the stagger queue serially
*/
private async processQueue(): Promise<void> {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
const item = this.queue.shift()!;
try {
logger.log('info', `Processing cert provision for ${item.domain}`);
const result = await item.fn();
item.resolve(result);
} catch (err) {
item.reject(err);
}
// Stagger delay between provisions
if (this.queue.length > 0) {
await new Promise<void>((r) => setTimeout(r, this.staggerDelayMs));
}
}
this.processing = false;
}
}

View File

@@ -13,6 +13,8 @@ import {
import { logger } from './logger.js'; import { logger } from './logger.js';
// Import storage manager // Import storage manager
import { StorageManager, type IStorageConfig } from './storage/index.js'; import { StorageManager, type IStorageConfig } from './storage/index.js';
import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
// Import cache system // Import cache system
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js'; import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
@@ -122,7 +124,7 @@ export interface IDcRouterOptions {
cacheConfig?: { cacheConfig?: {
/** Enable cache database (default: true) */ /** Enable cache database (default: true) */
enabled?: boolean; enabled?: boolean;
/** Storage path for TsmDB data (default: /etc/dcrouter/tsmdb) */ /** Storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */
storagePath?: string; storagePath?: string;
/** Database name (default: dcrouter) */ /** Database name (default: dcrouter) */
dbName?: string; dbName?: string;
@@ -171,6 +173,7 @@ export class DcRouter {
// Core services // Core services
public smartProxy?: plugins.smartproxy.SmartProxy; public smartProxy?: plugins.smartproxy.SmartProxy;
public smartAcme?: plugins.smartacme.SmartAcme;
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer; public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
public emailServer?: UnifiedEmailServer; public emailServer?: UnifiedEmailServer;
public radiusServer?: RadiusServer; public radiusServer?: RadiusServer;
@@ -182,6 +185,19 @@ export class DcRouter {
public cacheDb?: CacheDb; public cacheDb?: CacheDb;
public cacheCleaner?: CacheCleaner; 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 backoff + stagger
public certProvisionScheduler?: CertProvisionScheduler;
// TypedRouter for API endpoints // TypedRouter for API endpoints
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -193,7 +209,14 @@ export class DcRouter {
this.options = { this.options = {
...optionsArg ...optionsArg
}; };
// Default storage to filesystem if not configured
if (!this.options.storage) {
this.options.storage = {
fsPath: plugins.path.join(paths.dcrouterHomeDir, 'storage'),
};
}
// Initialize storage manager // Initialize storage manager
this.storageManager = new StorageManager(this.options.storage); this.storageManager = new StorageManager(this.options.storage);
} }
@@ -349,7 +372,7 @@ export class DcRouter {
// Initialize CacheDb singleton // Initialize CacheDb singleton
this.cacheDb = CacheDb.getInstance({ this.cacheDb = CacheDb.getInstance({
storagePath: cacheConfig.storagePath || '/etc/dcrouter/tsmdb', storagePath: cacheConfig.storagePath || paths.defaultTsmDbPath,
dbName: cacheConfig.dbName || 'dcrouter', dbName: cacheConfig.dbName || 'dcrouter',
debug: false, debug: false,
}); });
@@ -426,15 +449,81 @@ export class DcRouter {
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
...this.options.smartProxyConfig, ...this.options.smartProxyConfig,
routes, routes,
acme: acmeConfig acme: acmeConfig,
certStore: {
loadAll: async () => {
const keys = await this.storageManager.list('/proxy-certs/');
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
for (const key of keys) {
const data = await this.storageManager.getJSON(key);
if (data) certs.push(data);
}
return certs;
},
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
domain, publicKey, privateKey, ca,
});
},
remove: async (domain: string) => {
await this.storageManager.delete(`/proxy-certs/${domain}`);
},
},
}; };
// 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) { if (challengeHandlers.length > 0) {
// We'll need to pass this to SmartProxy somehow this.smartAcme = new plugins.smartacme.SmartAcme({
// For now, we'll set it as a property accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
(smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers; certManager: new StorageBackedCertManager(this.storageManager),
(smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01']; 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 {
const result = await scheduler.enqueueProvision(domain, async () => {
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
eventComms.setSource('smartacme-dns-01');
const cert = await this.smartAcme.getCertificateForDomain(domain);
if (cert.validUntil) {
eventComms.setExpiryDate(new Date(cert.validUntil));
}
return {
id: cert.id,
domainName: cert.domainName,
created: cert.created,
validUntil: cert.validUntil,
privateKey: cert.privateKey,
publicKey: cert.publicKey,
csr: cert.csr,
};
});
// 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 // Create SmartProxy instance
@@ -453,19 +542,36 @@ export class DcRouter {
console.error('[DcRouter] Error stack:', err.stack); console.error('[DcRouter] Error stack:', err.stack);
}); });
if (acmeConfig) { // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
this.smartProxy.on('certificate-issued', (event) => { // Events are keyed by domain for domain-centric certificate tracking
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`); 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 // Start SmartProxy
console.log('[DcRouter] Starting SmartProxy...'); console.log('[DcRouter] Starting SmartProxy...');
@@ -568,29 +674,6 @@ export class DcRouter {
emailRoutes.push(routeConfig); emailRoutes.push(routeConfig);
} }
// Add email domain-based routes if configured
if (emailConfig.routes) {
for (const route of emailConfig.routes) {
emailRoutes.push({
name: route.name,
match: {
ports: emailConfig.ports,
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
},
action: {
type: 'forward',
targets: route.action.type === 'forward' && route.action.forward ? [{
host: route.action.forward.host,
port: route.action.forward.port || 25
}] : undefined,
tls: {
mode: 'passthrough'
}
}
});
}
}
return emailRoutes; return emailRoutes;
} }
@@ -637,27 +720,66 @@ export class DcRouter {
* @returns Whether the domain matches the pattern * @returns Whether the domain matches the pattern
*/ */
private isDomainMatch(domain: string, pattern: string): boolean { private isDomainMatch(domain: string, pattern: string): boolean {
// Normalize inputs
domain = domain.toLowerCase(); domain = domain.toLowerCase();
pattern = pattern.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('*.')) { if (pattern.startsWith('*.')) {
const patternSuffix = pattern.slice(2); // Remove the "*." prefix const suffix = pattern.slice(2);
if (domain === suffix) return true;
// Check if domain ends with the pattern suffix and has at least one character before it return domain.endsWith(suffix) && domain.length > suffix.length;
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
} }
// No match
return false; 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() { public async stop() {
console.log('Stopping DcRouter services...'); console.log('Stopping DcRouter services...');
@@ -675,6 +797,9 @@ export class DcRouter {
// Stop unified email server if running // Stop unified email server if running
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(), 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 // Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(), 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, requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
throughput: proxyMetrics ? { throughput: proxyMetrics ? {
bytesIn: proxyMetrics.totals.bytesIn(), bytesIn: proxyMetrics.totals.bytesIn(),
bytesOut: proxyMetrics.totals.bytesOut() bytesOut: proxyMetrics.totals.bytesOut(),
} : { bytesIn: 0, bytesOut: 0 }, bytesInPerSecond: proxyMetrics.throughput.instant().in,
bytesOutPerSecond: proxyMetrics.throughput.instant().out,
} : { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
}; };
}); });
} }
@@ -482,40 +484,58 @@ export class MetricsManager {
// Use shorter cache TTL for network stats to ensure real-time updates // Use shorter cache TTL for network stats to ensure real-time updates
return this.metricsCache.get('networkStats', () => { return this.metricsCache.get('networkStats', () => {
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
if (!proxyMetrics) { if (!proxyMetrics) {
return { return {
connectionsByIP: new Map<string, number>(), connectionsByIP: new Map<string, number>(),
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
topIPs: [], topIPs: [] as Array<{ ip: string; count: number }>,
totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, 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,
}; };
} }
// Get metrics using the new API // Get metrics using the new API
const connectionsByIP = proxyMetrics.connections.byIP(); const connectionsByIP = proxyMetrics.connections.byIP();
const instantThroughput = proxyMetrics.throughput.instant(); const instantThroughput = proxyMetrics.throughput.instant();
// Get throughput rate // Get throughput rate
const throughputRate = { const throughputRate = {
bytesInPerSecond: instantThroughput.in, bytesInPerSecond: instantThroughput.in,
bytesOutPerSecond: instantThroughput.out bytesOutPerSecond: instantThroughput.out
}; };
// Get top IPs // Get top IPs
const topIPs = proxyMetrics.connections.topIPs(10); const topIPs = proxyMetrics.connections.topIPs(10);
// Get total data transferred // Get total data transferred
const totalDataTransferred = { const totalDataTransferred = {
bytesIn: proxyMetrics.totals.bytesIn(), bytesIn: proxyMetrics.totals.bytesIn(),
bytesOut: proxyMetrics.totals.bytesOut() 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 { return {
connectionsByIP, connectionsByIP,
throughputRate, throughputRate,
topIPs, topIPs,
totalDataTransferred, totalDataTransferred,
throughputHistory,
throughputByIP,
requestsPerSecond,
requestsTotal,
}; };
}, 200); // Use 200ms cache for more frequent updates }, 200); // Use 200ms cache for more frequent updates
} }

View File

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

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

View File

@@ -84,21 +84,37 @@ export class SecurityHandler {
// Get network stats from MetricsManager if available // Get network stats from MetricsManager if available
if (this.opsServerRef.dcRouterRef.metricsManager) { if (this.opsServerRef.dcRouterRef.metricsManager) {
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); 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 { return {
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })), connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
throughputRate: networkStats.throughputRate, throughputRate: networkStats.throughputRate,
topIPs: networkStats.topIPs, topIPs: networkStats.topIPs,
totalDataTransferred: networkStats.totalDataTransferred, totalDataTransferred: networkStats.totalDataTransferred,
throughputHistory: networkStats.throughputHistory || [],
throughputByIP,
requestsPerSecond: networkStats.requestsPerSecond || 0,
requestsTotal: networkStats.requestsTotal || 0,
}; };
} }
// Fallback if MetricsManager not available // Fallback if MetricsManager not available
return { return {
connectionsByIP: [], connectionsByIP: [],
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
topIPs: [], topIPs: [],
totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
throughputHistory: [],
throughputByIP: [],
requestsPerSecond: 0,
requestsTotal: 0,
}; };
} }
) )

View File

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

View File

@@ -8,11 +8,17 @@ export const packageDir = plugins.path.join(
); );
export const distServe = plugins.path.join(packageDir, './dist_serve'); export const distServe = plugins.path.join(packageDir, './dist_serve');
// Configure data directory with environment variable or default to .nogit/data // Default base for all dcrouter data (always user-writable)
const DEFAULT_DATA_PATH = '.nogit/data'; export const dcrouterHomeDir = plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
export const dataDir = process.env.DATA_DIR
? process.env.DATA_DIR // Configure data directory with environment variable or default to ~/.serve.zone/dcrouter/data
: plugins.path.join(baseDir, DEFAULT_DATA_PATH); const DEFAULT_DATA_PATH = plugins.path.join(dcrouterHomeDir, 'data');
export const dataDir = process.env.DATA_DIR
? process.env.DATA_DIR
: DEFAULT_DATA_PATH;
// Default TsmDB path for CacheDb
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
// MTA directories // MTA directories
export const keysDir = plugins.path.join(dataDir, 'keys'); export const keysDir = plugins.path.join(dataDir, 'keys');

View File

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

View File

@@ -89,7 +89,7 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
#### 🔐 Authentication #### 🔐 Authentication
| Interface | Method | Description | | Interface | Method | Description |
|-----------|--------|-------------| |-----------|--------|-------------|
| `IReq_AdminLoginWithUsernameAndPassword` | `adminLogin` | Authenticate as admin | | `IReq_AdminLoginWithUsernameAndPassword` | `adminLoginWithUsernameAndPassword` | Authenticate as admin |
| `IReq_AdminLogout` | `adminLogout` | End admin session | | `IReq_AdminLogout` | `adminLogout` | End admin session |
| `IReq_VerifyIdentity` | `verifyIdentity` | Verify JWT token validity | | `IReq_VerifyIdentity` | `verifyIdentity` | Verify JWT token validity |

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

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

View File

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

View File

@@ -47,12 +47,25 @@ export interface INetworkState {
connections: interfaces.data.IConnectionInfo[]; connections: interfaces.data.IConnectionInfo[];
connectionsByIP: { [ip: string]: number }; connectionsByIP: { [ip: string]: number };
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
totalBytes: { in: number; out: number };
topIPs: Array<{ ip: string; count: 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; lastUpdated: number;
isLoading: boolean; isLoading: boolean;
error: string | null; 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 { export interface IEmailOpsState {
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security'; currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
queuedEmails: interfaces.requests.IEmailQueueItem[]; queuedEmails: interfaces.requests.IEmailQueueItem[];
@@ -103,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
// Determine initial view from URL path // Determine initial view from URL path
const getInitialView = (): string => { const getInitialView = (): string => {
const path = typeof window !== 'undefined' ? window.location.pathname : '/'; 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 segments = path.split('/').filter(Boolean);
const view = segments[0]; const view = segments[0];
return validViews.includes(view) ? view : 'overview'; return validViews.includes(view) ? view : 'overview';
@@ -136,7 +149,12 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
connections: [], connections: [],
connectionsByIP: {}, connectionsByIP: {},
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
totalBytes: { in: 0, out: 0 },
topIPs: [], topIPs: [],
throughputByIP: [],
throughputHistory: [],
requestsPerSecond: 0,
requestsTotal: 0,
lastUpdated: 0, lastUpdated: 0,
isLoading: false, isLoading: false,
error: null, error: null,
@@ -162,6 +180,18 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
'soft' '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 // Actions for state management
interface IActionContext { interface IActionContext {
identity: interfaces.data.IIdentity | null; identity: interfaces.data.IIdentity | null;
@@ -340,7 +370,14 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
networkStatePart.dispatchAction(fetchNetworkStatsAction, null); networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
}, 100); }, 100);
} }
// If switching to certificates view, ensure we fetch certificate data
if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
setTimeout(() => {
certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
}, 100);
}
return { return {
...currentState, ...currentState,
activeView: viewName, activeView: viewName,
@@ -394,7 +431,14 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
connections: connectionsResponse.connections, connections: connectionsResponse.connections,
connectionsByIP, connectionsByIP,
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, 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 || [], topIPs: networkStatsResponse.topIPs || [],
throughputByIP: networkStatsResponse.throughputByIP || [],
throughputHistory: networkStatsResponse.throughputHistory || [],
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
requestsTotal: networkStatsResponse.requestsTotal || 0,
lastUpdated: Date.now(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
error: null, 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 // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();
@@ -703,7 +807,12 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in, bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out bytesOutPerSecond: network.totalBandwidth.out
}, },
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), 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(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
error: null, error: null,
@@ -718,13 +827,27 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in, bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out bytesOutPerSecond: network.totalBandwidth.out
}, },
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), 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(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
error: null, 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) { } catch (error) {
console.error('Combined refresh failed:', error); console.error('Combined refresh failed:', error);
} }
@@ -749,13 +872,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
refreshInterval = setInterval(() => { refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency // Use combined refresh action for efficiency
dispatchCombinedRefreshAction(); 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); }, uiState.refreshInterval);
} }
} else { } else {

View File

@@ -5,4 +5,5 @@ export * from './ops-view-emails.js';
export * from './ops-view-logs.js'; export * from './ops-view-logs.js';
export * from './ops-view-config.js'; export * from './ops-view-config.js';
export * from './ops-view-security.js'; export * from './ops-view-security.js';
export * from './ops-view-certificates.js';
export * from './shared/index.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 { OpsViewLogs } from './ops-view-logs.js';
import { OpsViewConfig } from './ops-view-config.js'; import { OpsViewConfig } from './ops-view-config.js';
import { OpsViewSecurity } from './ops-view-security.js'; import { OpsViewSecurity } from './ops-view-security.js';
import { OpsViewCertificates } from './ops-view-certificates.js';
@customElement('ops-dashboard') @customElement('ops-dashboard')
export class OpsDashboard extends DeesElement { export class OpsDashboard extends DeesElement {
@@ -61,6 +62,10 @@ export class OpsDashboard extends DeesElement {
name: 'Security', name: 'Security',
element: OpsViewSecurity, 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 requestCountHistory = new Map<number, number>(); // Track requests per time bucket
private trafficUpdateTimer: any = null; private trafficUpdateTimer: any = null;
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
private historyLoaded = false; // Whether server-side throughput history has been loaded
// Removed byte tracking - now using real-time data from SmartProxy
constructor() { constructor() {
super(); super();
@@ -95,7 +94,7 @@ export class OpsViewNetwork extends DeesElement {
// Fixed 5 minute time range // Fixed 5 minute time range
const range = 5 * 60 * 1000; // 5 minutes const range = 5 * 60 * 1000; // 5 minutes
const bucketSize = range / 60; // 60 data points const bucketSize = range / 60; // 60 data points
// Initialize with empty data points for both in and out // Initialize with empty data points for both in and out
const emptyData = Array.from({ length: 60 }, (_, i) => { const emptyData = Array.from({ length: 60 }, (_, i) => {
const time = now - ((59 - i) * bucketSize); const time = now - ((59 - i) * bucketSize);
@@ -104,13 +103,61 @@ export class OpsViewNetwork extends DeesElement {
y: 0, y: 0,
}; };
}); });
this.trafficDataIn = [...emptyData]; this.trafficDataIn = [...emptyData];
this.trafficDataOut = emptyData.map(point => ({ ...point })); this.trafficDataOut = emptyData.map(point => ({ ...point }));
this.lastTrafficUpdateTime = now; 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 = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
viewHostCss, viewHostCss,
@@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement {
return `${size.toFixed(1)} ${units[unitIndex]}`; 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 } { private calculateThroughput(): { in: number; out: number } {
// Use real throughput data from network state // Use real throughput data from network state
return { return {
@@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement {
} }
private renderNetworkStats(): TemplateResult { 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 throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0; 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]; const trendData = [...this.requestsPerSecHistory];
// If we don't have enough data, pad with zeros
while (trendData.length < 20) { while (trendData.length < 20) {
trendData.unshift(0); trendData.unshift(0);
} }
@@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number', type: 'number',
icon: 'plug', icon: 'plug',
color: activeConnections > 100 ? '#f59e0b' : '#22c55e', color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`, description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
actions: [ actions: [
{ {
name: 'View Details', name: 'View Details',
@@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement {
icon: 'chartLine', icon: 'chartLine',
color: '#3b82f6', color: '#3b82f6',
trendData: trendData, trendData: trendData,
description: `Average over last minute`, description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
}, },
{ {
id: 'throughputIn', id: 'throughputIn',
@@ -426,6 +459,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number', type: 'number',
icon: 'download', icon: 'download',
color: '#22c55e', color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
}, },
{ {
id: 'throughputOut', id: 'throughputOut',
@@ -435,6 +469,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number', type: 'number',
icon: 'upload', icon: 'upload',
color: '#8b5cf6', color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
}, },
]; ];
@@ -460,20 +495,33 @@ export class OpsViewNetwork extends DeesElement {
if (this.networkState.topIPs.length === 0) { if (this.networkState.topIPs.length === 0) {
return html``; 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 // Calculate total connections across all top IPs
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0); const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
return html` return html`
<dees-table <dees-table
.data=${this.networkState.topIPs} .data=${this.networkState.topIPs}
.displayFunction=${(ipData: { ip: string; count: number }) => ({ .displayFunction=${(ipData: { ip: string; count: number }) => {
'IP Address': ipData.ip, const bw = bandwidthByIP.get(ipData.ip);
'Connections': ipData.count, return {
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%', 'IP Address': ipData.ip,
})} 'Connections': ipData.count,
'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" heading1="Top Connected IPs"
heading2="IPs with most active connections" heading2="IPs with most active connections and bandwidth"
.pagination=${false} .pagination=${false}
dataName="ip" dataName="ip"
></dees-table> ></dees-table>
@@ -513,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
} }
} }
// Generate traffic data based on request history // Load server-side throughput history into chart (once)
this.updateTrafficData(); 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() { private startTrafficUpdateTimer() {

View File

@@ -126,12 +126,26 @@ export class OpsViewOverview extends DeesElement {
const units = ['B', 'KB', 'MB', 'GB', 'TB']; const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes; let size = bytes;
let unitIndex = 0; let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) { while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024; size /= 1024;
unitIndex++; unitIndex++;
} }
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]}`; return `${size.toFixed(1)} ${units[unitIndex]}`;
} }
@@ -162,6 +176,24 @@ export class OpsViewOverview extends DeesElement {
color: '#3b82f6', color: '#3b82f6',
description: `Total: ${this.statsState.serverStats.totalConnections}`, 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', id: 'cpu',
title: 'CPU Usage', title: 'CPU Usage',

View File

@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter; 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 const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
export type TValidView = typeof validViews[number]; export type TValidView = typeof validViews[number];