Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 822bbc1957 | |||
| eacddc7ce1 | |||
| dc6ce341bd | |||
| 1aadc93f92 | |||
| 8fdcd479d6 | |||
| d24dde8eff | |||
| 40a34073e9 | |||
| 9ac297c197 | |||
| ddd0662fb8 | |||
| 11bc0dde6c | |||
| 610d691244 | |||
| c88410ea53 | |||
| 9cbdd24281 | |||
| dce1de8c4b | |||
| 86e6c4f600 | |||
| 0618755236 | |||
| b21f3385e1 | |||
| dd61e0c962 | |||
| ac3a42fc41 | |||
| c23f16149c | |||
| 529a4bae00 | |||
| 49606ae007 | |||
| 31a6510d8b | |||
| b5e760ae07 | |||
| ea32babaac | |||
| a4ddedaf46 |
112
changelog.md
112
changelog.md
@@ -1,5 +1,117 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-19 - 7.1.0 - feat(ops/monitoring)
|
||||||
|
add in-memory log buffer, metrics time-series and ops UI integration
|
||||||
|
|
||||||
|
- bump @push.rocks/smartlog to ^3.2.0
|
||||||
|
- introduce SmartlogDestinationBuffer (logBuffer) and wire it into the base logger to provide an in-memory log store for the Ops UI
|
||||||
|
- implement minute-resolution time-series buckets in MetricsManager with increment/prune helpers and new APIs getEmailTimeSeries and getDnsTimeSeries
|
||||||
|
- sync security counters from SecurityLogger and expose recent security events via StatsHandler
|
||||||
|
- wire email delivery lifecycle events and bounce processing to MetricsManager for tracking sent/received/failed metrics
|
||||||
|
- LogsHandler now queries the in-memory log buffer, maps smartlog levels/categories, supports search/level/time-range filtering and pagination
|
||||||
|
- UI updates: ops-view-overview, ops-view-logs and ops-view-security consume time-series and recent events to render charts, tables and filters
|
||||||
|
|
||||||
|
## 2026-02-19 - 7.0.1 - fix(monitoring)
|
||||||
|
Use smartMetrics cpuPercentage for cpuUsage.user and update smartmetrics and smartproxy dependencies
|
||||||
|
|
||||||
|
- Switch cpuUsage.user from parseFloat(smartMetricsData.cpuUsageText) to smartMetricsData.cpuPercentage to align with smartmetrics v3 API
|
||||||
|
- Bump @push.rocks/smartmetrics from ^2.0.10 to ^3.0.1
|
||||||
|
- Bump @push.rocks/smartproxy from ^25.7.6 to ^25.7.8
|
||||||
|
|
||||||
|
## 2026-02-19 - 7.0.0 - BREAKING CHANGE(deps)
|
||||||
|
bump dependencies: @serve.zone/remoteingress to ^4.0.0 (breaking), @push.rocks/smartproxy to ^25.7.6, @types/node to ^25.3.0
|
||||||
|
|
||||||
|
- Updated @serve.zone/remoteingress from ^3.3.0 to ^4.0.0 — major breaking change; may require code changes to adapt to new API.
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.7.3 to ^25.7.6 — patch update (non-breaking).
|
||||||
|
- Updated @types/node from ^25.2.3 to ^25.3.0 — patch update (non-breaking).
|
||||||
|
- Current package version is 6.13.2; recommend bumping to 7.0.0 due to the breaking dependency upgrade.
|
||||||
|
|
||||||
|
## 2026-02-19 - 6.13.2 - fix(runtime)
|
||||||
|
prevent memory leaks and improve shutdown/stream handling across services
|
||||||
|
|
||||||
|
- Add CertProvisionScheduler.clear() to reset in-memory backoff cache and call it during DcRouter shutdown
|
||||||
|
- Stop any existing SmartAcme instance before creating a new one (await stop and log errors) to avoid duplicate running instances
|
||||||
|
- Null out many DcRouter service references and clear certificateStatusMap on shutdown to allow GC of stopped services
|
||||||
|
- Cap emailMetrics.recipients map size and trim to ~80% of MAX_TOP_DOMAINS to prevent unbounded growth
|
||||||
|
- Await virtualStream.sendData in logs follow handler and clear the interval if the stream errors/closes to avoid interval leaks
|
||||||
|
- Limit normalizedMacCache size and evict oldest entries when it exceeds 10000 to prevent unbounded cache growth
|
||||||
|
|
||||||
|
## 2026-02-18 - 6.13.1 - fix(dcrouter)
|
||||||
|
enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs
|
||||||
|
|
||||||
|
- Set smartProxyConfig.acceptProxyProtocol = true when options.remoteIngressConfig.enabled
|
||||||
|
- Whitelist loopback address by setting smartProxyConfig.proxyIPs = ['127.0.0.1']
|
||||||
|
- Only applies when remoteIngress is enabled; used to accept tunneled connections forwarded by the hub to preserve original client IPs
|
||||||
|
|
||||||
|
## 2026-02-18 - 6.13.0 - feat(remoteingress)
|
||||||
|
include listenPorts for allowed edges sent to the Rust hub and always resync allowed edges when edge properties change
|
||||||
|
|
||||||
|
- getAllowedEdges now returns listenPorts for each allowed edge (uses getEffectiveListenPorts)
|
||||||
|
- remoteingress handler now calls tunnelManager.syncAllowedEdges() whenever tunnelManager exists so ports/tags/enabled changes are propagated
|
||||||
|
- Improves Rust hub routing by providing per-edge listening ports and ensuring allowed-edge list is kept up-to-date
|
||||||
|
|
||||||
|
## 2026-02-18 - 6.12.0 - feat(remote-ingress)
|
||||||
|
add Remote Ingress hub integration, OpsServer UI, APIs, and docs
|
||||||
|
|
||||||
|
- Integrates RemoteIngress (hub/tunnel) into DcRouter: runtime manager, tunnel manager and Rust data plane references added
|
||||||
|
- Bumps dependency @serve.zone/remoteingress to ^3.3.0
|
||||||
|
- Adds configuration defaults and IDcRouterOptions.remoteIngressConfig with tunnelPort/hubDomain/tls fields
|
||||||
|
- Introduces OpsServer API endpoints and TypedRequest methods for remote ingress: getRemoteIngresses, createRemoteIngress, updateRemoteIngress, deleteRemoteIngress, regenerateRemoteIngressSecret, getRemoteIngressStatus, getRemoteIngressConnectionToken
|
||||||
|
- UI updates: new Remote Ingress dashboard view, connection token generation & copy (clipboard API + fallback), auto-derived ports display, and toast notifications
|
||||||
|
- State/API rename: newEdgeSecret -> newEdgeId and clearNewEdgeIdAction; appstate fetchConnectionToken usage
|
||||||
|
- Documentation: README, ts/ and ts_web readmes, and ts_interfaces updated with interfaces and examples for Remote Ingress
|
||||||
|
- Minor UI icon updates (search -> fa:magnifyingGlass, clipboard icon casing) and other doc/README improvements
|
||||||
|
|
||||||
|
## 2026-02-18 - 6.11.0 - feat(remoteingress)
|
||||||
|
add ability to generate remote ingress connection tokens and UI copy action; add hubDomain config option; update remoteingress dependency to ^3.1.1
|
||||||
|
|
||||||
|
- Add server typed handler 'getRemoteIngressConnectionToken' to generate an encoded connection token containing hubHost, hubPort, edgeId and secret.
|
||||||
|
- Add request interface IReq_GetRemoteIngressConnectionToken for typed requests.
|
||||||
|
- Add fetchConnectionToken helper in web appstate and a 'Copy Token' action in ops-view-remoteingress to copy tokens to the clipboard with toast feedback.
|
||||||
|
- Add hubDomain option to remoteIngressConfig in dcrouter options so an external hostname can be embedded in connection tokens.
|
||||||
|
- Bump dependency @serve.zone/remoteingress from ^3.0.4 to ^3.1.1 in package.json.
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.10.0 - feat(ops-view-certificates)
|
||||||
|
Make Export and Delete actions available inline (inRow) as well as in the context menu; bump @design.estate/dees-catalog to ^3.43.0
|
||||||
|
|
||||||
|
- Added 'inRow' to action types for 'Export' and 'Delete' in ts_web/elements/ops-view-certificates.ts to expose actions inline in the row
|
||||||
|
- Updated dependency @design.estate/dees-catalog from ^3.42.2 to ^3.43.0 in package.json
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.9.0 - feat(certificates)
|
||||||
|
add certificate import, export, and deletion support (server handlers, request types, and UI)
|
||||||
|
|
||||||
|
- Add typed request handlers in opsserver: deleteCertificate, exportCertificate, importCertificate (ts/opsserver/handlers/certificate.handler.ts)
|
||||||
|
- Implement deleteCertificate/exportCertificate/importCertificate functions handling storage paths, in-memory status map updates, backoff clearing, validation, and SmartAcme-compatible /certs/ and /proxy-certs/ formats
|
||||||
|
- Add request interfaces IReq_DeleteCertificate, IReq_ExportCertificate, IReq_ImportCertificate (ts_interfaces/requests/certificate.ts)
|
||||||
|
- Add web app actions deleteCertificateAction, importCertificateAction and fetchCertificateExport to call new typed requests (ts_web/appstate.ts)
|
||||||
|
- Update certificates UI to support Import, Export, and Delete actions and add downloadJsonFile helper (ts_web/elements/ops-view-certificates.ts)
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.8.0 - feat(remote-ingress)
|
||||||
|
support auto-deriving ports for remote ingress edges and expose manual/derived port breakdown in API and UI
|
||||||
|
|
||||||
|
- Add autoDerivePorts flag to IRemoteIngress with default true and migration to set existing stored edges to autoDerivePorts = true
|
||||||
|
- RemoteIngressManager: getEffectiveListenPorts now returns the union of manual + derived ports when autoDerivePorts is enabled; added getPortBreakdown to return manual vs derived lists
|
||||||
|
- API handlers updated: create/update requests accept autoDerivePorts; responses now include effectiveListenPorts, manualPorts, and derivedPorts (secrets still masked)
|
||||||
|
- Web UI updated: create and edit dialogs include an Auto-derive checkbox; port badges now visually distinguish manual vs derived ports; added updateRemoteIngressAction
|
||||||
|
- Non-breaking change: new field defaults to true so existing behavior is preserved
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.7.0 - feat(remote-ingress)
|
||||||
|
Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI
|
||||||
|
|
||||||
|
- Add effectiveListenPorts?: number[] to IRemoteIngress interface (present in API responses)
|
||||||
|
- Make createRemoteIngressAction.listenPorts optional and update creation modal to allow empty ports (auto-derived)
|
||||||
|
- Add toggleRemoteIngressAction to enable/disable remote ingress edges and wire up Enable/Disable row/context-menu actions
|
||||||
|
- Update getPortsHtml to prefer manual listenPorts, fall back to effectiveListenPorts, show '(auto)' when derived and 'none' when no ports
|
||||||
|
- Standardize UI actions to use inRow/contextmenu and actionFunc signatures; update create modal to use explicit Cancel/Create menu options and collect form data programmatically
|
||||||
|
|
||||||
|
## 2026-02-17 - 6.6.1 - fix(icons)
|
||||||
|
standardize icon identifiers to lucide-prefixed names across operational views
|
||||||
|
|
||||||
|
- Replaced legacy/ambiguous icon names with 'lucide:...' identifiers in four UI modules: ts_web/elements/ops-view-certificates.ts, ops-view-network.ts, ops-view-overview.ts, and ops-view-security.ts.
|
||||||
|
- Updated common action/menu icons (e.g. arrowsRotate -> lucide:RefreshCw, magnifyingGlass -> lucide:Search, copy -> lucide:Copy, fileExport -> lucide:FileOutput).
|
||||||
|
- Mapped dashboard/tile icons to lucide equivalents (e.g. server -> lucide:Server, networkWired/sitemap -> lucide:Network, download/upload -> lucide:Download/Upload, microchip/memory -> lucide:Cpu/MemoryStick).
|
||||||
|
- Normalized alert and status icons to lucide names (e.g. triangleExclamation -> lucide:TriangleAlert, shield/userShield -> lucide:Shield/ShieldCheck, clock/clockRotateLeft -> lucide:Clock/History).
|
||||||
|
|
||||||
## 2026-02-17 - 6.6.0 - feat(remoteingress)
|
## 2026-02-17 - 6.6.0 - feat(remoteingress)
|
||||||
derive effective remote ingress listen ports from route configs and expose them via ops API
|
derive effective remote ingress listen ports from route configs and expose them via ops API
|
||||||
|
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "6.6.0",
|
"version": "7.1.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": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@git.zone/tswatch": "^3.1.0",
|
"@git.zone/tswatch": "^3.1.0",
|
||||||
"@types/node": "^25.2.3"
|
"@types/node": "^25.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.6",
|
"@api.global/typedrequest": "^3.2.6",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"@api.global/typedserver": "^8.3.0",
|
"@api.global/typedserver": "^8.3.0",
|
||||||
"@api.global/typedsocket": "^4.1.0",
|
"@api.global/typedsocket": "^4.1.0",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.42.2",
|
"@design.estate/dees-catalog": "^3.43.0",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
@@ -42,21 +42,21 @@
|
|||||||
"@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.11",
|
"@push.rocks/smartlog": "^3.2.0",
|
||||||
"@push.rocks/smartmetrics": "^2.0.10",
|
"@push.rocks/smartmetrics": "^3.0.1",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartmta": "^5.2.2",
|
"@push.rocks/smartmta": "^5.2.2",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.7.3",
|
"@push.rocks/smartproxy": "^25.7.8",
|
||||||
"@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",
|
||||||
"@push.rocks/smartstate": "^2.0.30",
|
"@push.rocks/smartstate": "^2.0.30",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^3.0.4",
|
"@serve.zone/remoteingress": "^4.0.0",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
"lru-cache": "^11.2.6",
|
"lru-cache": "^11.2.6",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
|
|||||||
342
pnpm-lock.yaml
generated
342
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.42.2
|
specifier: ^3.43.0
|
||||||
version: 3.42.2(@tiptap/pm@2.27.2)
|
version: 3.43.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.1.6
|
specifier: ^2.1.6
|
||||||
version: 2.1.6
|
version: 2.1.6
|
||||||
@@ -54,11 +54,11 @@ 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.11
|
specifier: ^3.2.0
|
||||||
version: 3.1.11
|
version: 3.2.0
|
||||||
'@push.rocks/smartmetrics':
|
'@push.rocks/smartmetrics':
|
||||||
specifier: ^2.0.10
|
specifier: ^3.0.1
|
||||||
version: 2.0.10
|
version: 3.0.1
|
||||||
'@push.rocks/smartmongo':
|
'@push.rocks/smartmongo':
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(socks@2.8.7)
|
version: 5.1.0(socks@2.8.7)
|
||||||
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^25.7.3
|
specifier: ^25.7.8
|
||||||
version: 25.7.3
|
version: 25.7.8
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -96,8 +96,8 @@ importers:
|
|||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
'@serve.zone/remoteingress':
|
'@serve.zone/remoteingress':
|
||||||
specifier: ^3.0.4
|
specifier: ^4.0.0
|
||||||
version: 3.0.4
|
version: 4.0.0
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
@@ -124,8 +124,8 @@ importers:
|
|||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
version: 3.1.0(@tiptap/pm@2.27.2)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.2.3
|
specifier: ^25.3.0
|
||||||
version: 25.2.3
|
version: 25.3.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -351,8 +351,8 @@ packages:
|
|||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.42.2':
|
'@design.estate/dees-catalog@3.43.0':
|
||||||
resolution: {integrity: sha512-e/d5XpIjuOmQIxHnBq81Uq+TyBHX92Ie1n7jEFBCYtxvi3+P2LU1sQ3VDrvLTpkwGxq7iyagu7BYWHYRtPLPmw==}
|
resolution: {integrity: sha512-UFW8oThP9Mc4L0wVVgmuGux868Ct/TwZ1WP8hZCe4e/+5gmxDc+4EArnt5hePHENboe1Soobh9mmrMN6kQZ3xQ==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -681,74 +681,74 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.4.6':
|
'@mongodb-js/saslprep@1.4.6':
|
||||||
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.92':
|
'@napi-rs/canvas-android-arm64@0.1.93':
|
||||||
resolution: {integrity: sha512-rDOtq53ujfOuevD5taxAuIFALuf1QsQWZe1yS/N4MtT+tNiDBEdjufvQRPWZ11FubL2uwgP8ApYU3YOaNu1ZsQ==}
|
resolution: {integrity: sha512-xRIoOPFvneR29Dtq5d9p2AJbijDCFeV4jQ+5Ms/xVAXJVb8R0Jlu+pPr/SkhrG+Mouaml4roPSXugTIeRl6CMA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.92':
|
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
||||||
resolution: {integrity: sha512-4PT6GRGCr7yMRehp42x0LJb1V0IEy1cDZDDayv7eKbFUIGbPFkV7CRC9Bee5MPkjg1EB4ZPXXUyy3gjQm7mR8Q==}
|
resolution: {integrity: sha512-daNDi76HN5grC6GXDmpxdfP+N2mQPd3sCfg62VyHwUuvbZh32P7R/IUjkzAxtYMtTza+Zvx9hfLJ3J7ENL6WMA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.92':
|
'@napi-rs/canvas-darwin-x64@0.1.93':
|
||||||
resolution: {integrity: sha512-5e/3ZapP7CqPtDcZPtmowCsjoyQwuNMMD7c0GKPtZQ8pgQhLkeq/3fmk0HqNSD1i227FyJN/9pDrhw/UMTkaWA==}
|
resolution: {integrity: sha512-1YfuNPIQLawsg/gSNdJRk4kQWUy9M/Gy8FGsOI79nhQEJ2PZdqpSPl5UNzf4elfuNXuVbEbmmjP68EQdUunDuQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.92':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
||||||
resolution: {integrity: sha512-j6KaLL9iir68lwpzzY+aBGag1PZp3+gJE2mQ3ar4VJVmyLRVOh+1qsdNK1gfWoAVy5w6U7OEYFrLzN2vOFUSng==}
|
resolution: {integrity: sha512-8kEkOQPZjuyHjupvXExuJZiuiVNecdABGq3DLI7aO1EvQFOOlWMm2d/8Q5qXdV73Tn+nu3m16+kPajsN1oJefQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
||||||
resolution: {integrity: sha512-s3NlnJMHOSotUYVoTCoC1OcomaChFdKmZg0VsHFeIkeHbwX0uPHP4eCX1irjSfMykyvsGHTQDfBAtGYuqxCxhQ==}
|
resolution: {integrity: sha512-qIKLKkBkYSyWSYAoDThoxf5y1gr4X0g7W8rDU7d2HDeAAcotdVHUwuKkMeNe6+5VNk7/95EIhbslQjSxiCu32g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.92':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
||||||
resolution: {integrity: sha512-xV0GQnukYq5qY+ebkAwHjnP2OrSGBxS3vSi1zQNQj0bkXU6Ou+Tw7JjCM7pZcQ28MUyEBS1yKfo7rc7ip2IPFQ==}
|
resolution: {integrity: sha512-mAwQBGM3qArS9XEO21AK4E1uGvCuUCXjhIZk0dlVvs49MQ6wAAuCkYKNFpSKeSicKrLWwBMfgWX4qZoPh+M00A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
||||||
resolution: {integrity: sha512-+GKvIFbQ74eB/TopEdH6XIXcvOGcuKvCITLGXy7WLJAyNp3Kdn1ncjxg91ihatBaPR+t63QOE99yHuIWn3UQ9w==}
|
resolution: {integrity: sha512-kaIH5MpPzOZfkM+QMsBxGdM9jlJT+N+fwz2IEaju/S+DL65E5TgPOx4QcD5dQ8vsMxlak6uDrudBc4ns5xzZCw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
||||||
resolution: {integrity: sha512-tFd6MwbEhZ1g64iVY2asV+dOJC+GT3Yd6UH4G3Hp0/VHQ6qikB+nvXEULskFYZ0+wFqlGPtXjG1Jmv7sJy+3Ww==}
|
resolution: {integrity: sha512-KtMZJqYWvOSeW5w3VSV2f5iGnwNdKJm4gwgVid4xNy1NFi+NJSyuglA1lX1u4wIPxizyxh8OW5c5Usf6oSOMNQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.92':
|
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
||||||
resolution: {integrity: sha512-uSuqeSveB/ZGd72VfNbHCSXO9sArpZTvznMVsb42nqPP7gBGEH6NJQ0+hmF+w24unEmxBhPYakP/Wiosm16KkA==}
|
resolution: {integrity: sha512-qRZhOvlDBooRLX6V3/t9X9B+plZK+OrPLgfFixu0A1RO/3VHbubOknfnMnocSDAqk/L6cRyKI83VP2ciR9UO7w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.92':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
||||||
resolution: {integrity: sha512-20SK5AU/OUNz9ZuoAPj5ekWai45EIBDh/XsdrVZ8le/pJVlhjFU3olbumSQUXRFn7lBRS+qwM8kA//uLaDx6iQ==}
|
resolution: {integrity: sha512-um5XE44vF8bjkQEsH2iRSUP9fDeQGYbn/qjM/v4whXG83qsqapAXlOPOQqSARZB1SiNvPUAuXoRsJLlKFmAEFw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.92':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
||||||
resolution: {integrity: sha512-KEhyZLzq1MXCNlXybz4k25MJmHFp+uK1SIb8yJB0xfrQjz5aogAMhyseSzewo+XxAq3OAOdyKvfHGNzT3w1RPg==}
|
resolution: {integrity: sha512-maHlizZgmKsAPJwjwBZMnsWfq3Ca9QutoteQwKe7YqsmbECoylrLCCOGCDOredstW4BRWqRTfCl6NJaVVeAQvQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.92':
|
'@napi-rs/canvas@0.1.93':
|
||||||
resolution: {integrity: sha512-q7ZaUCJkEU5BeOdE7fBx1XWRd2T5Ady65nxq4brMf5L4cE1VV/ACq5w9Z5b/IVJs8CwSSIwc30nlthH0gFo4Ig==}
|
resolution: {integrity: sha512-unVFo8CUlUeJCCxt50+j4yy91NF4x6n9zdGcvEsOFAWzowtZm3mgx8X2D7xjwV0cFSfxmpGPoe+JS77uzeFsxg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -757,10 +757,6 @@ packages:
|
|||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0':
|
'@oxc-project/types@0.99.0':
|
||||||
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
||||||
|
|
||||||
@@ -964,8 +960,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.11':
|
'@push.rocks/smartlog@3.2.0':
|
||||||
resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==}
|
resolution: {integrity: sha512-d6IzsSG8HTmgxr8c9BVzZWpn8m3c17b5O+orHdwrlgHqPVa0+WXMe1ezItuPVZH5q6i8h+OfCv382PTloNiweg==}
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
||||||
@@ -979,8 +975,8 @@ packages:
|
|||||||
'@push.rocks/smartmatch@2.0.0':
|
'@push.rocks/smartmatch@2.0.0':
|
||||||
resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==}
|
resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==}
|
||||||
|
|
||||||
'@push.rocks/smartmetrics@2.0.10':
|
'@push.rocks/smartmetrics@3.0.1':
|
||||||
resolution: {integrity: sha512-Fr4ZzJWFqTR67ThmPsj+uUfPoWZ+p87n7wItpXVjlQ2mf4pboWnsfmrrC1gqIXca1Dwa2i7L+GPoJ3hOZ+Qhfw==}
|
resolution: {integrity: sha512-wdc9v8F0dQXwraAVCZjGvt2lnGpbVYHWzER9OFR+u+ultMq7aLjUevxSpkS8BkhBMLTuNMCCWIqyAzaCqHzFgQ==}
|
||||||
|
|
||||||
'@push.rocks/smartmime@1.0.6':
|
'@push.rocks/smartmime@1.0.6':
|
||||||
resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==}
|
resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==}
|
||||||
@@ -1034,8 +1030,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.7.3':
|
'@push.rocks/smartproxy@25.7.8':
|
||||||
resolution: {integrity: sha512-9b5dwsLAhuDqnJptGBum4qBHlZwZPqPG3CJKxAwE3uFKjCmcE8qGDwodI0CjrQ7KW2PJ1BMq/Lk4ghs3Da6PWw==}
|
resolution: {integrity: sha512-rKuC/5DgCBQmk1iCY2mZd+ZdH2mBOfcP1hWMARTP4Je4KqnNTJ2STM1tJmc9FmKVXxtEQCxWJnEnq1wNqwQFRA==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1131,9 +1127,6 @@ 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==}
|
||||||
|
|
||||||
@@ -1340,8 +1333,8 @@ packages:
|
|||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||||
|
|
||||||
'@serve.zone/remoteingress@3.0.4':
|
'@serve.zone/remoteingress@4.0.0':
|
||||||
resolution: {integrity: sha512-ZD66Y8fvW7SjealziOlhaC7+Y/3gxQkZlj/X8rxgVHmGhlc/YQtn6H6LNVazbM88BXK5ns004Qo6ongAB6Ho0Q==}
|
resolution: {integrity: sha512-kcC9pnQdS5Mw0ypmwT3GIYwrjVQJOjWw5TJ+j46t6Y98S4Mb7wEAAPsU+YRXozpkqUxWUDBeDK1fQHLvoF9PmQ==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
@@ -1834,11 +1827,8 @@ packages:
|
|||||||
'@types/node@22.19.11':
|
'@types/node@22.19.11':
|
||||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||||
|
|
||||||
'@types/node@25.2.3':
|
'@types/node@25.3.0':
|
||||||
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
|
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||||
|
|
||||||
'@types/pidusage@2.0.5':
|
|
||||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
|
||||||
|
|
||||||
'@types/ping@0.4.4':
|
'@types/ping@0.4.4':
|
||||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||||
@@ -1966,8 +1956,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
apexcharts@5.5.0:
|
apexcharts@5.6.0:
|
||||||
resolution: {integrity: sha512-r0GzBUmIAihVDHiPTWrKzd2I+T2Dw+oZTDBRJeBExUuCyqEaCe2pAMEKZnTbJQXyDAhCBzPgkM2SeeKQuW4Ddw==}
|
resolution: {integrity: sha512-BZua59yedRsaDfnxkzNrkyLCvluq2c3ZDBIz4joxSKtgr0xDQXQ5dzceMhf/TpTbAjaF+2NYIpLP3BEEIG2s/w==}
|
||||||
|
|
||||||
argparse@1.0.10:
|
argparse@1.0.10:
|
||||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||||
@@ -2059,9 +2049,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
|
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
bintrees@1.0.2:
|
|
||||||
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
|
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3238,6 +3225,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
|
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
minimatch@10.2.1:
|
||||||
|
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
|
||||||
|
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==}
|
||||||
|
|
||||||
@@ -3548,15 +3539,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pidtree@0.6.0:
|
|
||||||
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
|
||||||
engines: {node: '>=0.10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
pidusage@4.0.1:
|
|
||||||
resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
ping@0.4.4:
|
ping@0.4.4:
|
||||||
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
|
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@@ -3576,10 +3558,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
prom-client@15.1.3:
|
|
||||||
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
|
|
||||||
engines: {node: ^16 || ^18 || >=20}
|
|
||||||
|
|
||||||
property-information@7.1.0:
|
property-information@7.1.0:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
@@ -3610,8 +3588,8 @@ packages:
|
|||||||
prosemirror-markdown@1.13.4:
|
prosemirror-markdown@1.13.4:
|
||||||
resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==}
|
resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==}
|
||||||
|
|
||||||
prosemirror-menu@1.2.5:
|
prosemirror-menu@1.3.0:
|
||||||
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
|
resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==}
|
||||||
|
|
||||||
prosemirror-model@1.25.4:
|
prosemirror-model@1.25.4:
|
||||||
resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
|
resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
|
||||||
@@ -3978,9 +3956,6 @@ packages:
|
|||||||
tar-stream@3.1.7:
|
tar-stream@3.1.7:
|
||||||
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
|
||||||
|
|
||||||
tdigest@0.1.2:
|
|
||||||
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
|
||||||
|
|
||||||
text-decoder@1.2.3:
|
text-decoder@1.2.3:
|
||||||
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
||||||
|
|
||||||
@@ -4089,8 +4064,8 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici-types@7.16.0:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
@@ -4325,7 +4300,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -4365,7 +4340,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||||
'@cloudflare/workers-types': 4.20260210.0
|
'@cloudflare/workers-types': 4.20260210.0
|
||||||
'@design.estate/dees-catalog': 3.42.2(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.43.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4374,7 +4349,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -4441,7 +4416,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -4963,7 +4938,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.42.2(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.43.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.8
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
@@ -4983,7 +4958,7 @@ snapshots:
|
|||||||
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||||
'@tiptap/starter-kit': 2.27.2
|
'@tiptap/starter-kit': 2.27.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
apexcharts: 5.5.0
|
apexcharts: 5.6.0
|
||||||
highlight.js: 11.11.1
|
highlight.js: 11.11.1
|
||||||
ibantools: 4.5.1
|
ibantools: 4.5.1
|
||||||
lucide: 0.564.0
|
lucide: 0.564.0
|
||||||
@@ -5175,7 +5150,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.11
|
'@push.rocks/smartlog': 3.2.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
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -5196,7 +5171,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5222,7 +5197,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5257,7 +5232,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5303,7 +5278,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5472,52 +5447,52 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.92':
|
'@napi-rs/canvas-android-arm64@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.92':
|
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.92':
|
'@napi-rs/canvas-darwin-x64@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.92':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.92':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.92':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.92':
|
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.92':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.92':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.92':
|
'@napi-rs/canvas@0.1.93':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.92
|
'@napi-rs/canvas-android-arm64': 0.1.93
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.92
|
'@napi-rs/canvas-darwin-arm64': 0.1.93
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.92
|
'@napi-rs/canvas-darwin-x64': 0.1.93
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.92
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.93
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.92
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.93
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.92
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.93
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.92
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.93
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.92
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.93
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.92
|
'@napi-rs/canvas-linux-x64-musl': 0.1.93
|
||||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.92
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.93
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.92
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.93
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -5534,8 +5509,6 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0': {}
|
'@oxc-project/types@0.99.0': {}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
@@ -5738,7 +5711,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.11
|
'@push.rocks/smartlog': 3.2.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/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -5762,7 +5735,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
|
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
|
||||||
@@ -5773,7 +5746,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdata': 7.0.15(socks@2.8.7)
|
'@push.rocks/smartdata': 7.0.15(socks@2.8.7)
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartdns': 7.8.1
|
'@push.rocks/smartdns': 7.8.1
|
||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
@@ -5883,7 +5856,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5908,7 +5881,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -5937,7 +5910,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@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
|
||||||
@@ -6129,7 +6102,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.11':
|
'@push.rocks/smartlog@3.2.0':
|
||||||
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
|
||||||
@@ -6138,8 +6111,8 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@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.2.3
|
||||||
'@push.rocks/webrequest': 4.0.1
|
'@push.rocks/webrequest': 4.0.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
@@ -6173,14 +6146,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
matcher: 5.0.0
|
matcher: 5.0.0
|
||||||
|
|
||||||
'@push.rocks/smartmetrics@2.0.10':
|
'@push.rocks/smartmetrics@3.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@types/pidusage': 2.0.5
|
|
||||||
pidtree: 0.6.0
|
|
||||||
pidusage: 4.0.1
|
|
||||||
prom-client: 15.1.3
|
|
||||||
|
|
||||||
'@push.rocks/smartmime@1.0.6':
|
'@push.rocks/smartmime@1.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6249,7 +6218,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@push.rocks/smartmail': 2.2.0
|
'@push.rocks/smartmail': 2.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
@@ -6352,13 +6321,13 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.7.3':
|
'@push.rocks/smartproxy@25.7.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
minimatch: 10.2.0
|
minimatch: 10.2.1
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6436,7 +6405,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.11
|
'@push.rocks/smartlog': 3.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6471,7 +6440,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.11
|
'@push.rocks/smartlog': 3.2.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
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6586,7 +6555,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.11
|
'@push.rocks/smartlog': 3.2.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
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6602,7 +6571,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.11
|
'@push.rocks/smartlog': 3.2.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
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6618,7 +6587,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.11
|
'@push.rocks/smartlog': 3.2.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
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
@@ -6637,14 +6606,6 @@ 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
|
||||||
@@ -6830,7 +6791,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
'@serve.zone/remoteingress@3.0.4':
|
'@serve.zone/remoteingress@4.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
@@ -7301,7 +7262,7 @@ snapshots:
|
|||||||
prosemirror-inputrules: 1.5.1
|
prosemirror-inputrules: 1.5.1
|
||||||
prosemirror-keymap: 1.2.3
|
prosemirror-keymap: 1.2.3
|
||||||
prosemirror-markdown: 1.13.4
|
prosemirror-markdown: 1.13.4
|
||||||
prosemirror-menu: 1.2.5
|
prosemirror-menu: 1.3.0
|
||||||
prosemirror-model: 1.25.4
|
prosemirror-model: 1.25.4
|
||||||
prosemirror-schema-basic: 1.2.4
|
prosemirror-schema-basic: 1.2.4
|
||||||
prosemirror-schema-list: 1.5.1
|
prosemirror-schema-list: 1.5.1
|
||||||
@@ -7362,22 +7323,22 @@ snapshots:
|
|||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/buffer-json@2.0.3': {}
|
'@types/buffer-json@2.0.3': {}
|
||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7385,7 +7346,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.1.1':
|
'@types/express-serve-static-core@5.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 1.2.1
|
'@types/send': 1.2.1
|
||||||
@@ -7398,17 +7359,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/from2@2.3.6':
|
'@types/from2@2.3.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7430,12 +7391,12 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.10':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
@@ -7458,16 +7419,16 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mute-stream@0.0.4':
|
'@types/mute-stream@0.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
|
|
||||||
'@types/node-forge@1.3.14':
|
'@types/node-forge@1.3.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/node@18.19.130':
|
'@types/node@18.19.130':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7477,11 +7438,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@25.2.3':
|
'@types/node@25.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.18.2
|
||||||
|
|
||||||
'@types/pidusage@2.0.5': {}
|
|
||||||
|
|
||||||
'@types/ping@0.4.4': {}
|
'@types/ping@0.4.4': {}
|
||||||
|
|
||||||
@@ -7497,22 +7456,22 @@ snapshots:
|
|||||||
|
|
||||||
'@types/send@1.2.1':
|
'@types/send@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/serve-static@2.2.0':
|
'@types/serve-static@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/symbol-tree@3.2.5': {}
|
'@types/symbol-tree@3.2.5': {}
|
||||||
|
|
||||||
'@types/tar-stream@3.1.4':
|
'@types/tar-stream@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
@@ -7542,11 +7501,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@@ -7605,7 +7564,7 @@ snapshots:
|
|||||||
|
|
||||||
ansi-styles@6.2.3: {}
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
apexcharts@5.5.0:
|
apexcharts@5.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@yr/monotone-cubic-spline': 1.0.3
|
'@yr/monotone-cubic-spline': 1.0.3
|
||||||
|
|
||||||
@@ -7692,8 +7651,6 @@ snapshots:
|
|||||||
|
|
||||||
basic-ftp@5.1.0: {}
|
basic-ftp@5.1.0: {}
|
||||||
|
|
||||||
bintrees@1.0.2: {}
|
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -8019,7 +7976,7 @@ snapshots:
|
|||||||
engine.io@6.6.4:
|
engine.io@6.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cors': 2.8.19
|
'@types/cors': 2.8.19
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.0
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
@@ -9167,6 +9124,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.2
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
|
minimatch@10.2.1:
|
||||||
|
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
|
||||||
@@ -9427,7 +9388,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.92
|
'@napi-rs/canvas': 0.1.93
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
@@ -9439,12 +9400,6 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
pidtree@0.6.0: {}
|
|
||||||
|
|
||||||
pidusage@4.0.1:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
ping@0.4.4: {}
|
ping@0.4.4: {}
|
||||||
|
|
||||||
pkg-dir@4.2.0:
|
pkg-dir@4.2.0:
|
||||||
@@ -9459,11 +9414,6 @@ snapshots:
|
|||||||
|
|
||||||
progress@2.0.3: {}
|
progress@2.0.3: {}
|
||||||
|
|
||||||
prom-client@15.1.3:
|
|
||||||
dependencies:
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
tdigest: 0.1.2
|
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
|
|
||||||
prosemirror-changeset@2.4.0:
|
prosemirror-changeset@2.4.0:
|
||||||
@@ -9516,7 +9466,7 @@ snapshots:
|
|||||||
markdown-it: 14.1.1
|
markdown-it: 14.1.1
|
||||||
prosemirror-model: 1.25.4
|
prosemirror-model: 1.25.4
|
||||||
|
|
||||||
prosemirror-menu@1.2.5:
|
prosemirror-menu@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
crelt: 1.0.6
|
crelt: 1.0.6
|
||||||
prosemirror-commands: 1.7.1
|
prosemirror-commands: 1.7.1
|
||||||
@@ -10050,10 +10000,6 @@ snapshots:
|
|||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
- react-native-b4a
|
- react-native-b4a
|
||||||
|
|
||||||
tdigest@0.1.2:
|
|
||||||
dependencies:
|
|
||||||
bintrees: 1.0.2
|
|
||||||
|
|
||||||
text-decoder@1.2.3:
|
text-decoder@1.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
b4a: 1.7.3
|
b4a: 1.7.3
|
||||||
@@ -10152,7 +10098,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
173
readme.md
173
readme.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**dcrouter: The all-in-one gateway for your datacenter.** 🚀
|
**dcrouter: The all-in-one gateway for your datacenter.** 🚀
|
||||||
|
|
||||||
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS, and RADIUS protocols. Designed for enterprises requiring robust traffic management, automatic TLS certificate provisioning, and enterprise-grade email infrastructure — all from a single process.
|
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS, RADIUS, and remote edge ingress — all from a single process. Designed for enterprises requiring robust traffic management, automatic TLS certificate provisioning, distributed edge networking, and enterprise-grade email infrastructure.
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- [Email System](#email-system)
|
- [Email System](#email-system)
|
||||||
- [DNS Server](#dns-server)
|
- [DNS Server](#dns-server)
|
||||||
- [RADIUS Server](#radius-server)
|
- [RADIUS Server](#radius-server)
|
||||||
|
- [Remote Ingress](#remote-ingress)
|
||||||
- [Certificate Management](#certificate-management)
|
- [Certificate Management](#certificate-management)
|
||||||
- [Storage & Caching](#storage--caching)
|
- [Storage & Caching](#storage--caching)
|
||||||
- [Security Features](#security-features)
|
- [Security Features](#security-features)
|
||||||
@@ -60,6 +61,14 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **RADIUS accounting** for session tracking, traffic metering, and billing
|
- **RADIUS accounting** for session tracking, traffic metering, and billing
|
||||||
- **Real-time management** via OpsServer API
|
- **Real-time management** via OpsServer API
|
||||||
|
|
||||||
|
### 🌍 Remote Ingress (powered by [remoteingress](https://code.foss.global/serve.zone/remoteingress))
|
||||||
|
- **Distributed edge networking** — accept traffic at remote edge nodes and tunnel it to the hub
|
||||||
|
- **Edge registration CRUD** with secret-based authentication
|
||||||
|
- **Auto-derived ports** — edges automatically pick up ports from routes tagged with `remoteIngress.enabled`
|
||||||
|
- **Connection tokens** — generate a single opaque base64url token containing hubHost, hubPort, edgeId, and secret for easy edge provisioning
|
||||||
|
- **Real-time status monitoring** — connected/disconnected state, public IP, active tunnels, heartbeat tracking
|
||||||
|
- **OpsServer dashboard** with enable/disable, edit, secret regeneration, token copy, and delete actions
|
||||||
|
|
||||||
### ⚡ 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 MTA engine** via smartmta (TypeScript + Rust hybrid) for reliable email delivery
|
||||||
@@ -76,8 +85,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
### 🖥️ OpsServer Dashboard
|
### 🖥️ OpsServer Dashboard
|
||||||
- **Web-based management interface** with real-time monitoring
|
- **Web-based management interface** with real-time monitoring
|
||||||
- **JWT authentication** with session persistence
|
- **JWT authentication** with session persistence
|
||||||
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, and security events
|
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, and security events
|
||||||
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
||||||
|
- **Remote ingress management** with connection token generation and one-click copy
|
||||||
- **Read-only configuration display** — DcRouter is configured through code
|
- **Read-only configuration display** — DcRouter is configured through code
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -219,6 +229,13 @@ const router = new DcRouter({
|
|||||||
accounting: { enabled: true, retentionDays: 30 }
|
accounting: { enabled: true, retentionDays: 30 }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Remote Ingress — edge nodes tunnel traffic to this hub
|
||||||
|
remoteIngressConfig: {
|
||||||
|
enabled: true,
|
||||||
|
tunnelPort: 8443,
|
||||||
|
hubDomain: 'hub.example.com',
|
||||||
|
},
|
||||||
|
|
||||||
// Persistent storage
|
// Persistent storage
|
||||||
storage: { fsPath: '/var/lib/dcrouter/data' },
|
storage: { fsPath: '/var/lib/dcrouter/data' },
|
||||||
|
|
||||||
@@ -246,6 +263,7 @@ graph TB
|
|||||||
TCP[TCP Clients]
|
TCP[TCP Clients]
|
||||||
DNS[DNS Queries]
|
DNS[DNS Queries]
|
||||||
RAD[RADIUS Clients]
|
RAD[RADIUS Clients]
|
||||||
|
EDGE[Edge Nodes]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "DcRouter Core"
|
subgraph "DcRouter Core"
|
||||||
@@ -254,6 +272,7 @@ graph TB
|
|||||||
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
|
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
|
||||||
DS[SmartDNS Server<br/><i>Rust-powered</i>]
|
DS[SmartDNS Server<br/><i>Rust-powered</i>]
|
||||||
RS[SmartRadius Server]
|
RS[SmartRadius Server]
|
||||||
|
RI[RemoteIngress Hub<br/><i>Rust data plane</i>]
|
||||||
CM[Certificate Manager<br/><i>smartacme v9</i>]
|
CM[Certificate Manager<br/><i>smartacme v9</i>]
|
||||||
OS[OpsServer Dashboard]
|
OS[OpsServer Dashboard]
|
||||||
MM[Metrics Manager]
|
MM[Metrics Manager]
|
||||||
@@ -273,11 +292,13 @@ graph TB
|
|||||||
SMTP --> ES
|
SMTP --> ES
|
||||||
DNS --> DS
|
DNS --> DS
|
||||||
RAD --> RS
|
RAD --> RS
|
||||||
|
EDGE --> RI
|
||||||
|
|
||||||
DC --> SP
|
DC --> SP
|
||||||
DC --> ES
|
DC --> ES
|
||||||
DC --> DS
|
DC --> DS
|
||||||
DC --> RS
|
DC --> RS
|
||||||
|
DC --> RI
|
||||||
DC --> CM
|
DC --> CM
|
||||||
DC --> OS
|
DC --> OS
|
||||||
DC --> MM
|
DC --> MM
|
||||||
@@ -288,6 +309,7 @@ graph TB
|
|||||||
SP --> API
|
SP --> API
|
||||||
ES --> MAIL
|
ES --> MAIL
|
||||||
ES --> DB
|
ES --> DB
|
||||||
|
RI --> SP
|
||||||
|
|
||||||
CM -.-> SP
|
CM -.-> SP
|
||||||
CM -.-> ES
|
CM -.-> ES
|
||||||
@@ -303,6 +325,7 @@ graph TB
|
|||||||
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
|
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
|
||||||
| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting |
|
| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting |
|
||||||
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
|
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
|
||||||
|
| **RemoteIngress** | `@serve.zone/remoteingress` | Distributed edge tunneling with Rust data plane and TS management |
|
||||||
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
||||||
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
||||||
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
|
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
|
||||||
@@ -312,19 +335,20 @@ graph TB
|
|||||||
|
|
||||||
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
||||||
|
|
||||||
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, and SmartRadius based on which configs are provided.
|
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, and RemoteIngress 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. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
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. RemoteIngress runs a Rust data plane for edge tunnel networking. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
||||||
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
||||||
|
|
||||||
### Rust-Powered Architecture
|
### Rust-Powered Architecture
|
||||||
|
|
||||||
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.
|
DcRouter itself is a pure TypeScript orchestrator, but several 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 |
|
| Component | Rust Binary | What It Handles |
|
||||||
|-----------|-------------|-----------------|
|
|-----------|-------------|-----------------|
|
||||||
| **SmartProxy** | `smartproxy-bin` | All TCP/TLS/HTTP proxy networking, NFTables integration, connection metrics |
|
| **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 |
|
| **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 |
|
| **SmartDNS** | `smartdns-bin` | DNS server (UDP + DNS-over-HTTPS), DNSSEC, DNS client resolution |
|
||||||
|
| **RemoteIngress** | `remoteingress-bin` | Edge tunnel data plane, multiplexed streams, heartbeat management |
|
||||||
| **SmartRadius** | — | Pure TypeScript (no Rust component) |
|
| **SmartRadius** | — | Pure TypeScript (no Rust component) |
|
||||||
|
|
||||||
## Configuration Reference
|
## Configuration Reference
|
||||||
@@ -333,6 +357,10 @@ DcRouter itself is a pure TypeScript orchestrator, but three of its core sub-com
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IDcRouterOptions {
|
interface IDcRouterOptions {
|
||||||
|
// ── Base ───────────────────────────────────────────────────────
|
||||||
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
|
baseDir?: string;
|
||||||
|
|
||||||
// ── Traffic Routing ────────────────────────────────────────────
|
// ── Traffic Routing ────────────────────────────────────────────
|
||||||
/** SmartProxy config for HTTP/HTTPS and TCP/SNI routing */
|
/** SmartProxy config for HTTP/HTTPS and TCP/SNI routing */
|
||||||
smartProxyConfig?: ISmartProxyOptions;
|
smartProxyConfig?: ISmartProxyOptions;
|
||||||
@@ -376,6 +404,18 @@ interface IDcRouterOptions {
|
|||||||
accounting?: { enabled: boolean; retentionDays?: number };
|
accounting?: { enabled: boolean; retentionDays?: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Remote Ingress ─────────────────────────────────────────────
|
||||||
|
/** Remote Ingress hub for edge tunnel connections */
|
||||||
|
remoteIngressConfig?: {
|
||||||
|
enabled?: boolean; // default: false
|
||||||
|
tunnelPort?: number; // default: 8443
|
||||||
|
hubDomain?: string; // External hostname for connection tokens
|
||||||
|
tls?: {
|
||||||
|
certPath?: string;
|
||||||
|
keyPath?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// ── TLS & Certificates ────────────────────────────────────────
|
// ── TLS & Certificates ────────────────────────────────────────
|
||||||
tls?: {
|
tls?: {
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
@@ -701,6 +741,107 @@ RADIUS is fully manageable at runtime via the OpsServer API:
|
|||||||
- Session monitoring and forced disconnects
|
- Session monitoring and forced disconnects
|
||||||
- Accounting summaries and statistics
|
- Accounting summaries and statistics
|
||||||
|
|
||||||
|
## Remote Ingress
|
||||||
|
|
||||||
|
DcRouter can act as a **hub** for distributed edge nodes using [`@serve.zone/remoteingress`](https://code.foss.global/serve.zone/remoteingress). Edge nodes accept incoming traffic at remote locations and tunnel it back to the hub over a single multiplexed connection. This is ideal for scenarios where you need to accept traffic at multiple geographic locations but process it centrally.
|
||||||
|
|
||||||
|
### Enabling Remote Ingress
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const router = new DcRouter({
|
||||||
|
remoteIngressConfig: {
|
||||||
|
enabled: true,
|
||||||
|
tunnelPort: 8443,
|
||||||
|
hubDomain: 'hub.example.com', // Embedded in connection tokens
|
||||||
|
},
|
||||||
|
// Routes tagged with remoteIngress are auto-derived to edge listen ports
|
||||||
|
smartProxyConfig: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'web-via-edge',
|
||||||
|
match: { domains: ['app.example.com'], ports: [443] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' }
|
||||||
|
},
|
||||||
|
remoteIngress: { enabled: true } // Edges will listen on port 443
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge Registration
|
||||||
|
|
||||||
|
Edges are registered via the OpsServer API (or dashboard UI). Each edge gets a unique ID and secret:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Via TypedRequest API
|
||||||
|
const createReq = new TypedRequest<IReq_CreateRemoteIngress>(
|
||||||
|
'https://hub:3000/typedrequest', 'createRemoteIngress'
|
||||||
|
);
|
||||||
|
const { edge } = await createReq.fire({
|
||||||
|
identity,
|
||||||
|
name: 'edge-nyc-01',
|
||||||
|
autoDerivePorts: true,
|
||||||
|
tags: ['us-east'],
|
||||||
|
});
|
||||||
|
// edge.secret is returned only on creation — save it!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Tokens 🔑
|
||||||
|
|
||||||
|
Instead of configuring edges with four separate values (hubHost, hubPort, edgeId, secret), DcRouter can generate a single **connection token** — an opaque base64url string that encodes everything:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Via TypedRequest API
|
||||||
|
const tokenReq = new TypedRequest<IReq_GetRemoteIngressConnectionToken>(
|
||||||
|
'https://hub:3000/typedrequest', 'getRemoteIngressConnectionToken'
|
||||||
|
);
|
||||||
|
const { token } = await tokenReq.fire({ identity, edgeId: 'edge-uuid' });
|
||||||
|
// token = "eyJoIjoiaHViLmV4YW1wbGUuY29tIiwicCI6ODQ0MywiZSI6I..."
|
||||||
|
|
||||||
|
// On the edge side, just pass the token:
|
||||||
|
const edge = new RemoteIngressEdge({ token });
|
||||||
|
await edge.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
The token is generated using `remoteingress.encodeConnectionToken()` and contains `{ hubHost, hubPort, edgeId, secret }`. The `hubHost` comes from `remoteIngressConfig.hubDomain` (or can be overridden per-request).
|
||||||
|
|
||||||
|
In the OpsServer dashboard, click **"Copy Token"** on any edge row to copy the connection token to your clipboard.
|
||||||
|
|
||||||
|
### Auto-Derived Ports
|
||||||
|
|
||||||
|
When routes have `remoteIngress: { enabled: true }`, edges with `autoDerivePorts: true` (default) automatically pick up those routes' ports. You can also use `edgeFilter` to restrict which edges get which ports:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'web-route',
|
||||||
|
match: { ports: [443] },
|
||||||
|
action: { /* ... */ },
|
||||||
|
remoteIngress: {
|
||||||
|
enabled: true,
|
||||||
|
edgeFilter: ['us-east', 'edge-uuid-123'] // Only edges with matching id or tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard Actions
|
||||||
|
|
||||||
|
The OpsServer Remote Ingress view provides:
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| **Create Edge Node** | Register a new edge with name, ports, tags |
|
||||||
|
| **Enable / Disable** | Toggle an edge on or off |
|
||||||
|
| **Edit** | Modify name, manual ports, auto-derive setting, tags |
|
||||||
|
| **Regenerate Secret** | Issue a new secret (invalidates the old one) |
|
||||||
|
| **Copy Token** | Generate and copy a base64url connection token to clipboard |
|
||||||
|
| **Delete** | Remove the edge registration |
|
||||||
|
|
||||||
## Certificate Management
|
## Certificate Management
|
||||||
|
|
||||||
DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
|
DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
|
||||||
@@ -767,6 +908,7 @@ The OpsServer includes a **Certificates** view showing:
|
|||||||
- Expiry dates and issuer information
|
- Expiry dates and issuer information
|
||||||
- Backoff status for failed domains
|
- Backoff status for failed domains
|
||||||
- One-click reprovisioning per domain
|
- One-click reprovisioning per domain
|
||||||
|
- Certificate import and export
|
||||||
|
|
||||||
## Storage & Caching
|
## Storage & Caching
|
||||||
|
|
||||||
@@ -788,7 +930,7 @@ storage: {
|
|||||||
// Simply omit the storage config
|
// Simply omit the storage config
|
||||||
```
|
```
|
||||||
|
|
||||||
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state.
|
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state, remote ingress edge registrations.
|
||||||
|
|
||||||
### Cache Database
|
### Cache Database
|
||||||
|
|
||||||
@@ -874,7 +1016,8 @@ The OpsServer provides a web-based management interface served on port 3000. It'
|
|||||||
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
|
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
|
||||||
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
|
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
|
||||||
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
|
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
|
||||||
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning |
|
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export |
|
||||||
|
| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable |
|
||||||
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
||||||
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
||||||
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
|
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
|
||||||
@@ -906,6 +1049,18 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
|||||||
'getCertificateOverview' // Domain-centric certificate status
|
'getCertificateOverview' // Domain-centric certificate status
|
||||||
'reprovisionCertificate' // Reprovision by route name (legacy)
|
'reprovisionCertificate' // Reprovision by route name (legacy)
|
||||||
'reprovisionCertificateDomain' // Reprovision by domain (preferred)
|
'reprovisionCertificateDomain' // Reprovision by domain (preferred)
|
||||||
|
'importCertificate' // Import a certificate
|
||||||
|
'exportCertificate' // Export a certificate
|
||||||
|
'deleteCertificate' // Delete a certificate
|
||||||
|
|
||||||
|
// Remote Ingress
|
||||||
|
'getRemoteIngresses' // List all edge registrations
|
||||||
|
'createRemoteIngress' // Register a new edge
|
||||||
|
'updateRemoteIngress' // Update edge settings
|
||||||
|
'deleteRemoteIngress' // Remove an edge
|
||||||
|
'regenerateRemoteIngressSecret' // Issue a new secret
|
||||||
|
'getRemoteIngressStatus' // Runtime status of all edges
|
||||||
|
'getRemoteIngressConnectionToken' // Generate a connection token for an edge
|
||||||
|
|
||||||
// Configuration (read-only)
|
// Configuration (read-only)
|
||||||
'getConfiguration' // Current system config
|
'getConfiguration' // Current system config
|
||||||
@@ -957,6 +1112,8 @@ const router = new DcRouter(options: IDcRouterOptions);
|
|||||||
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
|
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
|
||||||
| `dnsServer` | `DnsServer` | DNS server instance |
|
| `dnsServer` | `DnsServer` | DNS server instance |
|
||||||
| `radiusServer` | `RadiusServer` | RADIUS server instance |
|
| `radiusServer` | `RadiusServer` | RADIUS server instance |
|
||||||
|
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
|
||||||
|
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
|
||||||
| `storageManager` | `StorageManager` | Storage backend |
|
| `storageManager` | `StorageManager` | Storage backend |
|
||||||
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
||||||
| `metricsManager` | `MetricsManager` | Metrics collector |
|
| `metricsManager` | `MetricsManager` | Metrics collector |
|
||||||
@@ -1000,7 +1157,7 @@ import { data, requests } from '@serve.zone/dcrouter/interfaces';
|
|||||||
DcRouter includes a comprehensive test suite covering all system components:
|
DcRouter includes a comprehensive test suite covering all system components:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests (10 files, 73 tests)
|
# Run all tests
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
# Run a specific test file
|
# Run a specific test file
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.6.0',
|
version: '7.1.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ export class CertProvisionScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all in-memory backoff cache entries
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.backoffCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get backoff info for UI display
|
* Get backoff info for UI display
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ export interface IDcRouterOptions {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/** Port for tunnel connections from edge nodes (default: 8443) */
|
/** Port for tunnel connections from edge nodes (default: 8443) */
|
||||||
tunnelPort?: number;
|
tunnelPort?: number;
|
||||||
|
/** External hostname of this hub, embedded in connection tokens */
|
||||||
|
hubDomain?: string;
|
||||||
/** TLS configuration for the tunnel server */
|
/** TLS configuration for the tunnel server */
|
||||||
tls?: {
|
tls?: {
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
@@ -532,6 +534,12 @@ export class DcRouter {
|
|||||||
|
|
||||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||||
if (challengeHandlers.length > 0) {
|
if (challengeHandlers.length > 0) {
|
||||||
|
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
||||||
|
if (this.smartAcme) {
|
||||||
|
await this.smartAcme.stop().catch(err =>
|
||||||
|
console.error('[DcRouter] Error stopping old SmartAcme:', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||||
certManager: new StorageBackedCertManager(this.storageManager),
|
certManager: new StorageBackedCertManager(this.storageManager),
|
||||||
@@ -584,6 +592,13 @@ export class DcRouter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When remoteIngress is enabled, the hub binary forwards tunneled connections
|
||||||
|
// to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
|
||||||
|
if (this.options.remoteIngressConfig?.enabled) {
|
||||||
|
smartProxyConfig.acceptProxyProtocol = true;
|
||||||
|
smartProxyConfig.proxyIPs = ['127.0.0.1'];
|
||||||
|
}
|
||||||
|
|
||||||
// Create SmartProxy instance
|
// Create SmartProxy instance
|
||||||
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
|
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
|
||||||
routeCount: smartProxyConfig.routes?.length,
|
routeCount: smartProxyConfig.routes?.length,
|
||||||
@@ -935,6 +950,25 @@ export class DcRouter {
|
|||||||
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear backoff cache in cert scheduler
|
||||||
|
if (this.certProvisionScheduler) {
|
||||||
|
this.certProvisionScheduler.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow GC of stopped services by nulling references
|
||||||
|
this.smartProxy = undefined;
|
||||||
|
this.emailServer = undefined;
|
||||||
|
this.dnsServer = undefined;
|
||||||
|
this.metricsManager = undefined;
|
||||||
|
this.cacheCleaner = undefined;
|
||||||
|
this.cacheDb = undefined;
|
||||||
|
this.tunnelManager = undefined;
|
||||||
|
this.radiusServer = undefined;
|
||||||
|
this.smartAcme = undefined;
|
||||||
|
this.certProvisionScheduler = undefined;
|
||||||
|
this.remoteIngressManager = undefined;
|
||||||
|
this.certificateStatusMap.clear();
|
||||||
|
|
||||||
console.log('All DcRouter services stopped');
|
console.log('All DcRouter services stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during DcRouter shutdown:', error);
|
console.error('Error during DcRouter shutdown:', error);
|
||||||
@@ -1022,6 +1056,24 @@ export class DcRouter {
|
|||||||
// Start the server
|
// Start the server
|
||||||
await this.emailServer.start();
|
await this.emailServer.start();
|
||||||
|
|
||||||
|
// Wire delivery events to MetricsManager for time-series tracking
|
||||||
|
if (this.metricsManager && this.emailServer.deliverySystem) {
|
||||||
|
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
|
||||||
|
this.metricsManager.trackEmailReceived(item?.from);
|
||||||
|
});
|
||||||
|
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
|
||||||
|
this.metricsManager.trackEmailSent(item?.to);
|
||||||
|
});
|
||||||
|
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
|
||||||
|
this.metricsManager.trackEmailFailed(item?.to, error?.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.metricsManager && this.emailServer) {
|
||||||
|
this.emailServer.on('bounceProcessed', () => {
|
||||||
|
this.metricsManager.trackEmailBounced();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
|
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { SmartlogDestinationBuffer } from '@push.rocks/smartlog/destination-buffer';
|
||||||
|
|
||||||
// Map NODE_ENV to valid TEnvironment
|
// Map NODE_ENV to valid TEnvironment
|
||||||
const nodeEnv = process.env.NODE_ENV || 'production';
|
const nodeEnv = process.env.NODE_ENV || 'production';
|
||||||
@@ -10,6 +11,9 @@ const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
|
|||||||
'production': 'production'
|
'production': 'production'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// In-memory log buffer for the OpsServer UI
|
||||||
|
export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 });
|
||||||
|
|
||||||
// Default Smartlog instance
|
// Default Smartlog instance
|
||||||
const baseLogger = new plugins.smartlog.Smartlog({
|
const baseLogger = new plugins.smartlog.Smartlog({
|
||||||
logContext: {
|
logContext: {
|
||||||
@@ -19,6 +23,9 @@ const baseLogger = new plugins.smartlog.Smartlog({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wire the buffer destination so all logs are captured
|
||||||
|
baseLogger.addLogDestination(logBuffer);
|
||||||
|
|
||||||
// Extended logger compatible with the original enhanced logger API
|
// Extended logger compatible with the original enhanced logger API
|
||||||
class StandardLogger {
|
class StandardLogger {
|
||||||
private defaultContext: Record<string, any> = {};
|
private defaultContext: Record<string, any> = {};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { DcRouter } from '../classes.dcrouter.js';
|
import { DcRouter } from '../classes.dcrouter.js';
|
||||||
import { MetricsCache } from './classes.metricscache.js';
|
import { MetricsCache } from './classes.metricscache.js';
|
||||||
|
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
|
||||||
|
|
||||||
export class MetricsManager {
|
export class MetricsManager {
|
||||||
private logger: plugins.smartlog.Smartlog;
|
private logger: plugins.smartlog.Smartlog;
|
||||||
@@ -37,6 +38,10 @@ export class MetricsManager {
|
|||||||
responseTimes: [] as number[], // Track response times in ms
|
responseTimes: [] as number[], // Track response times in ms
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Per-minute time-series buckets for charts
|
||||||
|
private emailMinuteBuckets = new Map<number, { sent: number; received: number; failed: number }>();
|
||||||
|
private dnsMinuteBuckets = new Map<number, { queries: number }>();
|
||||||
|
|
||||||
// Track security-specific metrics
|
// Track security-specific metrics
|
||||||
private securityMetrics = {
|
private securityMetrics = {
|
||||||
blockedIPs: 0,
|
blockedIPs: 0,
|
||||||
@@ -139,8 +144,8 @@ export class MetricsManager {
|
|||||||
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
||||||
},
|
},
|
||||||
cpuUsage: {
|
cpuUsage: {
|
||||||
user: parseFloat(smartMetricsData.cpuUsageText || '0'),
|
user: smartMetricsData.cpuPercentage,
|
||||||
system: 0, // SmartMetrics doesn't separate user/system
|
system: 0,
|
||||||
},
|
},
|
||||||
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
||||||
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
||||||
@@ -227,9 +232,34 @@ export class MetricsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync security metrics from the SecurityLogger singleton (last 24h).
|
||||||
|
* Called before returning security stats so counters reflect real events.
|
||||||
|
*/
|
||||||
|
private syncFromSecurityLogger(): void {
|
||||||
|
try {
|
||||||
|
const securityLogger = SecurityLogger.getInstance();
|
||||||
|
const summary = securityLogger.getEventsSummary(86400000); // last 24h
|
||||||
|
|
||||||
|
this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
|
||||||
|
this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
|
||||||
|
this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
|
||||||
|
this.securityMetrics.authFailures =
|
||||||
|
summary.byType[SecurityEventType.AUTHENTICATION] || 0;
|
||||||
|
this.securityMetrics.blockedIPs =
|
||||||
|
(summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
|
||||||
|
(summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
|
||||||
|
} catch {
|
||||||
|
// SecurityLogger may not be initialized yet — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get security metrics
|
// Get security metrics
|
||||||
public async getSecurityStats() {
|
public async getSecurityStats() {
|
||||||
return this.metricsCache.get('securityStats', () => {
|
return this.metricsCache.get('securityStats', () => {
|
||||||
|
// Sync counters from the real SecurityLogger events
|
||||||
|
this.syncFromSecurityLogger();
|
||||||
|
|
||||||
// Get recent incidents (last 20)
|
// Get recent incidents (last 20)
|
||||||
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
||||||
|
|
||||||
@@ -275,10 +305,19 @@ export class MetricsManager {
|
|||||||
// Email event tracking methods
|
// Email event tracking methods
|
||||||
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
||||||
this.emailMetrics.sentToday++;
|
this.emailMetrics.sentToday++;
|
||||||
|
this.incrementEmailBucket('sent');
|
||||||
|
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
||||||
this.emailMetrics.recipients.set(recipient, count + 1);
|
this.emailMetrics.recipients.set(recipient, count + 1);
|
||||||
|
|
||||||
|
// Cap recipients map to prevent unbounded growth within a day
|
||||||
|
if (this.emailMetrics.recipients.size > this.MAX_TOP_DOMAINS) {
|
||||||
|
const sorted = Array.from(this.emailMetrics.recipients.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8));
|
||||||
|
this.emailMetrics.recipients = new Map(sorted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deliveryTimeMs) {
|
if (deliveryTimeMs) {
|
||||||
@@ -303,6 +342,7 @@ export class MetricsManager {
|
|||||||
|
|
||||||
public trackEmailReceived(sender?: string): void {
|
public trackEmailReceived(sender?: string): void {
|
||||||
this.emailMetrics.receivedToday++;
|
this.emailMetrics.receivedToday++;
|
||||||
|
this.incrementEmailBucket('received');
|
||||||
|
|
||||||
this.emailMetrics.recentActivity.push({
|
this.emailMetrics.recentActivity.push({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -318,6 +358,7 @@ export class MetricsManager {
|
|||||||
|
|
||||||
public trackEmailFailed(recipient?: string, reason?: string): void {
|
public trackEmailFailed(recipient?: string, reason?: string): void {
|
||||||
this.emailMetrics.failedToday++;
|
this.emailMetrics.failedToday++;
|
||||||
|
this.incrementEmailBucket('failed');
|
||||||
|
|
||||||
this.emailMetrics.recentActivity.push({
|
this.emailMetrics.recentActivity.push({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -353,6 +394,7 @@ export class MetricsManager {
|
|||||||
// DNS event tracking methods
|
// DNS event tracking methods
|
||||||
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
|
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
|
||||||
this.dnsMetrics.totalQueries++;
|
this.dnsMetrics.totalQueries++;
|
||||||
|
this.incrementDnsBucket();
|
||||||
|
|
||||||
if (cacheHit) {
|
if (cacheHit) {
|
||||||
this.dnsMetrics.cacheHits++;
|
this.dnsMetrics.cacheHits++;
|
||||||
@@ -539,4 +581,90 @@ export class MetricsManager {
|
|||||||
};
|
};
|
||||||
}, 200); // Use 200ms cache for more frequent updates
|
}, 200); // Use 200ms cache for more frequent updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Time-series helpers ---
|
||||||
|
|
||||||
|
private static minuteKey(ts: number = Date.now()): number {
|
||||||
|
return Math.floor(ts / 60000) * 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
|
||||||
|
const key = MetricsManager.minuteKey();
|
||||||
|
let bucket = this.emailMinuteBuckets.get(key);
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = { sent: 0, received: 0, failed: 0 };
|
||||||
|
this.emailMinuteBuckets.set(key, bucket);
|
||||||
|
}
|
||||||
|
bucket[field]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private incrementDnsBucket(): void {
|
||||||
|
const key = MetricsManager.minuteKey();
|
||||||
|
let bucket = this.dnsMinuteBuckets.get(key);
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = { queries: 0 };
|
||||||
|
this.dnsMinuteBuckets.set(key, bucket);
|
||||||
|
}
|
||||||
|
bucket.queries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private pruneOldBuckets(): void {
|
||||||
|
const cutoff = Date.now() - 86400000; // 24h
|
||||||
|
for (const key of this.emailMinuteBuckets.keys()) {
|
||||||
|
if (key < cutoff) this.emailMinuteBuckets.delete(key);
|
||||||
|
}
|
||||||
|
for (const key of this.dnsMinuteBuckets.keys()) {
|
||||||
|
if (key < cutoff) this.dnsMinuteBuckets.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email time-series data for the last N hours, aggregated per minute.
|
||||||
|
*/
|
||||||
|
public getEmailTimeSeries(hours: number = 24): {
|
||||||
|
sent: Array<{ timestamp: number; value: number }>;
|
||||||
|
received: Array<{ timestamp: number; value: number }>;
|
||||||
|
failed: Array<{ timestamp: number; value: number }>;
|
||||||
|
} {
|
||||||
|
this.pruneOldBuckets();
|
||||||
|
const cutoff = Date.now() - hours * 3600000;
|
||||||
|
const sent: Array<{ timestamp: number; value: number }> = [];
|
||||||
|
const received: Array<{ timestamp: number; value: number }> = [];
|
||||||
|
const failed: Array<{ timestamp: number; value: number }> = [];
|
||||||
|
|
||||||
|
const sortedKeys = Array.from(this.emailMinuteBuckets.keys())
|
||||||
|
.filter((k) => k >= cutoff)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
const bucket = this.emailMinuteBuckets.get(key)!;
|
||||||
|
sent.push({ timestamp: key, value: bucket.sent });
|
||||||
|
received.push({ timestamp: key, value: bucket.received });
|
||||||
|
failed.push({ timestamp: key, value: bucket.failed });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sent, received, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DNS time-series data for the last N hours, aggregated per minute.
|
||||||
|
*/
|
||||||
|
public getDnsTimeSeries(hours: number = 24): {
|
||||||
|
queries: Array<{ timestamp: number; value: number }>;
|
||||||
|
} {
|
||||||
|
this.pruneOldBuckets();
|
||||||
|
const cutoff = Date.now() - hours * 3600000;
|
||||||
|
const queries: Array<{ timestamp: number; value: number }> = [];
|
||||||
|
|
||||||
|
const sortedKeys = Array.from(this.dnsMinuteBuckets.keys())
|
||||||
|
.filter((k) => k >= cutoff)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
const bucket = this.dnsMinuteBuckets.get(key)!;
|
||||||
|
queries.push({ timestamp: key, value: bucket.queries });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { queries };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,36 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Delete certificate
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
||||||
|
'deleteCertificate',
|
||||||
|
async (dataArg) => {
|
||||||
|
return this.deleteCertificate(dataArg.domain);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export certificate
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
||||||
|
'exportCertificate',
|
||||||
|
async (dataArg) => {
|
||||||
|
return this.exportCertificate(dataArg.domain);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import certificate
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
||||||
|
'importCertificate',
|
||||||
|
async (dataArg) => {
|
||||||
|
return this.importCertificate(dataArg.cert);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,4 +354,154 @@ export class CertificateHandler {
|
|||||||
|
|
||||||
return { success: false, message: `No routes found for domain '${domain}'` };
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete certificate data for a domain from storage
|
||||||
|
*/
|
||||||
|
private async deleteCertificate(domain: string): Promise<{ success: boolean; message?: string }> {
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||||
|
|
||||||
|
// Delete from all known storage paths
|
||||||
|
const paths = [
|
||||||
|
`/proxy-certs/${domain}`,
|
||||||
|
`/proxy-certs/${cleanDomain}`,
|
||||||
|
`/certs/${cleanDomain}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
try {
|
||||||
|
await dcRouter.storageManager.delete(path);
|
||||||
|
} catch {
|
||||||
|
// Path may not exist — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear from in-memory status map
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
|
||||||
|
// Clear backoff info
|
||||||
|
if (dcRouter.certProvisionScheduler) {
|
||||||
|
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: `Certificate data deleted for '${domain}'` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export certificate data for a domain as ICert-shaped JSON
|
||||||
|
*/
|
||||||
|
private async exportCertificate(domain: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
cert?: {
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||||
|
|
||||||
|
// Try SmartAcme /certs/ path first (has full ICert fields)
|
||||||
|
let certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (certData && certData.publicKey && certData.privateKey) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
cert: {
|
||||||
|
id: certData.id || plugins.crypto.randomUUID(),
|
||||||
|
domainName: certData.domainName || domain,
|
||||||
|
created: certData.created || Date.now(),
|
||||||
|
validUntil: certData.validUntil || 0,
|
||||||
|
privateKey: certData.privateKey,
|
||||||
|
publicKey: certData.publicKey,
|
||||||
|
csr: certData.csr || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try /proxy-certs/ with original domain
|
||||||
|
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${domain}`);
|
||||||
|
if (!certData || !certData.publicKey) {
|
||||||
|
// Try with clean domain
|
||||||
|
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${cleanDomain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certData && certData.publicKey && certData.privateKey) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
cert: {
|
||||||
|
id: plugins.crypto.randomUUID(),
|
||||||
|
domainName: domain,
|
||||||
|
created: certData.validFrom || Date.now(),
|
||||||
|
validUntil: certData.validUntil || 0,
|
||||||
|
privateKey: certData.privateKey,
|
||||||
|
publicKey: certData.publicKey,
|
||||||
|
csr: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: `No certificate data found for '${domain}'` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a certificate from ICert-shaped JSON
|
||||||
|
*/
|
||||||
|
private async importCertificate(cert: {
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
}): Promise<{ success: boolean; message?: string }> {
|
||||||
|
// Validate PEM content
|
||||||
|
if (!cert.publicKey || !cert.publicKey.includes('-----BEGIN CERTIFICATE-----')) {
|
||||||
|
return { success: false, message: 'Invalid publicKey: must contain a PEM-encoded certificate' };
|
||||||
|
}
|
||||||
|
if (!cert.privateKey || !cert.privateKey.includes('-----BEGIN')) {
|
||||||
|
return { success: false, message: 'Invalid privateKey: must contain a PEM-encoded key' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const cleanDomain = cert.domainName.replace(/^\*\.?/, '');
|
||||||
|
|
||||||
|
// Save to /certs/ (SmartAcme-compatible path)
|
||||||
|
await dcRouter.storageManager.setJSON(`/certs/${cleanDomain}`, {
|
||||||
|
id: cert.id,
|
||||||
|
domainName: cert.domainName,
|
||||||
|
created: cert.created,
|
||||||
|
validUntil: cert.validUntil,
|
||||||
|
privateKey: cert.privateKey,
|
||||||
|
publicKey: cert.publicKey,
|
||||||
|
csr: cert.csr || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also save to /proxy-certs/ (proxy-cert format)
|
||||||
|
await dcRouter.storageManager.setJSON(`/proxy-certs/${cert.domainName}`, {
|
||||||
|
domain: cert.domainName,
|
||||||
|
publicKey: cert.publicKey,
|
||||||
|
privateKey: cert.privateKey,
|
||||||
|
ca: undefined,
|
||||||
|
validUntil: cert.validUntil,
|
||||||
|
validFrom: cert.created,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update in-memory status map
|
||||||
|
dcRouter.certificateStatusMap.set(cert.domainName, {
|
||||||
|
status: 'valid',
|
||||||
|
source: 'static',
|
||||||
|
expiryDate: cert.validUntil ? new Date(cert.validUntil).toISOString() : undefined,
|
||||||
|
issuedAt: cert.created ? new Date(cert.created).toISOString() : undefined,
|
||||||
|
routeNames: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Certificate imported for '${cert.domainName}'` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
import { logBuffer } from '../../logger.js';
|
||||||
|
|
||||||
export class LogsHandler {
|
export class LogsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -64,6 +65,32 @@ export class LogsHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
|
||||||
|
switch (smartlogLevel) {
|
||||||
|
case 'silly':
|
||||||
|
case 'debug':
|
||||||
|
return 'debug';
|
||||||
|
case 'warn':
|
||||||
|
return 'warn';
|
||||||
|
case 'error':
|
||||||
|
return 'error';
|
||||||
|
default:
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static deriveCategory(
|
||||||
|
zone?: string,
|
||||||
|
message?: string
|
||||||
|
): 'smtp' | 'dns' | 'security' | 'system' | 'email' {
|
||||||
|
const msg = (message || '').toLowerCase();
|
||||||
|
if (msg.includes('[security:') || msg.includes('security')) return 'security';
|
||||||
|
if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta')) return 'email';
|
||||||
|
if (zone === 'dns' || msg.includes('dns')) return 'dns';
|
||||||
|
if (msg.includes('smtp')) return 'smtp';
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
|
||||||
private async getRecentLogs(
|
private async getRecentLogs(
|
||||||
level?: 'error' | 'warn' | 'info' | 'debug',
|
level?: 'error' | 'warn' | 'info' | 'debug',
|
||||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
|
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
|
||||||
@@ -78,9 +105,39 @@ export class LogsHandler {
|
|||||||
message: string;
|
message: string;
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
}>> {
|
}>> {
|
||||||
// TODO: Implement actual log retrieval from storage or logger
|
// Compute a timestamp cutoff from timeRange
|
||||||
// For now, return mock data
|
let since: number | undefined;
|
||||||
const mockLogs: Array<{
|
if (timeRange) {
|
||||||
|
const rangeMs: Record<string, number> = {
|
||||||
|
'1h': 3600000,
|
||||||
|
'6h': 21600000,
|
||||||
|
'24h': 86400000,
|
||||||
|
'7d': 604800000,
|
||||||
|
'30d': 2592000000,
|
||||||
|
};
|
||||||
|
since = Date.now() - (rangeMs[timeRange] || 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the UI level to smartlog levels for filtering
|
||||||
|
const smartlogLevels: string[] | undefined = level
|
||||||
|
? level === 'debug'
|
||||||
|
? ['debug', 'silly']
|
||||||
|
: level === 'info'
|
||||||
|
? ['info', 'ok', 'success', 'note', 'lifecycle']
|
||||||
|
: [level]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Fetch a larger batch from buffer, then apply category filter client-side
|
||||||
|
const rawEntries = logBuffer.getEntries({
|
||||||
|
level: smartlogLevels as any,
|
||||||
|
search,
|
||||||
|
since,
|
||||||
|
limit: limit * 3, // over-fetch to compensate for category filtering
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map ILogPackage → UI log format and apply category filter
|
||||||
|
const mapped: Array<{
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
level: 'debug' | 'info' | 'warn' | 'error';
|
level: 'debug' | 'info' | 'warn' | 'error';
|
||||||
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||||
@@ -88,32 +145,24 @@ export class LogsHandler {
|
|||||||
metadata?: any;
|
metadata?: any;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
for (const pkg of rawEntries) {
|
||||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
const uiLevel = LogsHandler.mapLogLevel(pkg.level);
|
||||||
const now = Date.now();
|
const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message);
|
||||||
|
|
||||||
// Generate some mock log entries
|
if (category && uiCategory !== category) continue;
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
|
||||||
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
|
||||||
|
|
||||||
// Filter by requested criteria
|
mapped.push({
|
||||||
if (level && mockLevel !== level) continue;
|
timestamp: pkg.timestamp,
|
||||||
if (category && mockCategory !== category) continue;
|
level: uiLevel,
|
||||||
|
category: uiCategory,
|
||||||
mockLogs.push({
|
message: pkg.message,
|
||||||
timestamp: now - (i * 60000), // 1 minute apart
|
metadata: pkg.data,
|
||||||
level: mockLevel,
|
|
||||||
category: mockCategory,
|
|
||||||
message: `Sample log message ${i} from ${mockCategory}`,
|
|
||||||
metadata: {
|
|
||||||
requestId: plugins.uuid.v4(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mapped.length >= limit) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pagination
|
return mapped;
|
||||||
return mockLogs.slice(offset, offset + limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupLogStream(
|
private setupLogStream(
|
||||||
@@ -148,7 +197,7 @@ export class LogsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For follow mode, simulate real-time log streaming
|
// For follow mode, simulate real-time log streaming
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(async () => {
|
||||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
||||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
||||||
|
|
||||||
@@ -171,7 +220,13 @@ export class LogsHandler {
|
|||||||
|
|
||||||
const logData = JSON.stringify(logEntry);
|
const logData = JSON.stringify(logEntry);
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
virtualStream.sendData(encoder.encode(logData));
|
try {
|
||||||
|
await virtualStream.sendData(encoder.encode(logData));
|
||||||
|
} catch {
|
||||||
|
// Stream closed or errored — clean up to prevent interval leak
|
||||||
|
clearInterval(intervalId!);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
}, 2000); // Send a log every 2 seconds
|
}, 2000); // Send a log every 2 seconds
|
||||||
|
|
||||||
// TODO: Hook into actual logger events
|
// TODO: Hook into actual logger events
|
||||||
|
|||||||
@@ -20,12 +20,17 @@ export class RemoteIngressHandler {
|
|||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { edges: [] };
|
return { edges: [] };
|
||||||
}
|
}
|
||||||
// Return edges without secrets, enriched with effective listen ports
|
// Return edges without secrets, enriched with effective listen ports and breakdown
|
||||||
const edges = manager.getAllEdges().map((e) => ({
|
const edges = manager.getAllEdges().map((e) => {
|
||||||
|
const breakdown = manager.getPortBreakdown(e);
|
||||||
|
return {
|
||||||
...e,
|
...e,
|
||||||
secret: '********', // Never expose secrets via API
|
secret: '********', // Never expose secrets via API
|
||||||
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
||||||
}));
|
manualPorts: breakdown.manual,
|
||||||
|
derivedPorts: breakdown.derived,
|
||||||
|
};
|
||||||
|
});
|
||||||
return { edges };
|
return { edges };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -50,6 +55,7 @@ export class RemoteIngressHandler {
|
|||||||
dataArg.name,
|
dataArg.name,
|
||||||
dataArg.listenPorts || [],
|
dataArg.listenPorts || [],
|
||||||
dataArg.tags,
|
dataArg.tags,
|
||||||
|
dataArg.autoDerivePorts ?? true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync allowed edges with the hub
|
// Sync allowed edges with the hub
|
||||||
@@ -102,6 +108,7 @@ export class RemoteIngressHandler {
|
|||||||
const edge = await manager.updateEdge(dataArg.id, {
|
const edge = await manager.updateEdge(dataArg.id, {
|
||||||
name: dataArg.name,
|
name: dataArg.name,
|
||||||
listenPorts: dataArg.listenPorts,
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
tags: dataArg.tags,
|
tags: dataArg.tags,
|
||||||
});
|
});
|
||||||
@@ -110,12 +117,22 @@ export class RemoteIngressHandler {
|
|||||||
return { success: false, edge: null as any };
|
return { success: false, edge: null as any };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync allowed edges if enabled status changed
|
// Sync allowed edges — ports, tags, or enabled may have changed
|
||||||
if (tunnelManager && dataArg.enabled !== undefined) {
|
if (tunnelManager) {
|
||||||
await tunnelManager.syncAllowedEdges();
|
await tunnelManager.syncAllowedEdges();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, edge: { ...edge, secret: '********' } };
|
const breakdown = manager.getPortBreakdown(edge);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
edge: {
|
||||||
|
...edge,
|
||||||
|
secret: '********',
|
||||||
|
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
||||||
|
manualPorts: breakdown.manual,
|
||||||
|
derivedPorts: breakdown.derived,
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -160,5 +177,46 @@ export class RemoteIngressHandler {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get a connection token for an edge
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||||
|
'getRemoteIngressConnectionToken',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'RemoteIngress not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const edge = manager.getEdge(dataArg.edgeId);
|
||||||
|
if (!edge) {
|
||||||
|
return { success: false, message: 'Edge not found' };
|
||||||
|
}
|
||||||
|
if (!edge.enabled) {
|
||||||
|
return { success: false, message: 'Edge is disabled' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hubHost = dataArg.hubHost
|
||||||
|
|| this.opsServerRef.dcRouterRef.options.remoteIngressConfig?.hubDomain;
|
||||||
|
if (!hubHost) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'No hub hostname configured. Set hubDomain in remoteIngressConfig or provide hubHost.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hubPort = this.opsServerRef.dcRouterRef.options.remoteIngressConfig?.tunnelPort ?? 8443;
|
||||||
|
|
||||||
|
const token = plugins.remoteingress.encodeConnectionToken({
|
||||||
|
hubHost,
|
||||||
|
hubPort,
|
||||||
|
edgeId: edge.id,
|
||||||
|
secret: edge.secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, token };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
import { MetricsManager } from '../../monitoring/index.js';
|
import { MetricsManager } from '../../monitoring/index.js';
|
||||||
|
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
||||||
|
|
||||||
export class StatsHandler {
|
export class StatsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -203,6 +204,11 @@ export class StatsHandler {
|
|||||||
if (sections.email) {
|
if (sections.email) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.collectEmailStats().then(stats => {
|
this.collectEmailStats().then(stats => {
|
||||||
|
// Get time-series data from MetricsManager
|
||||||
|
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
||||||
|
? this.opsServerRef.dcRouterRef.metricsManager.getEmailTimeSeries(24)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
metrics.email = {
|
metrics.email = {
|
||||||
sent: stats.sentToday,
|
sent: stats.sentToday,
|
||||||
received: stats.receivedToday,
|
received: stats.receivedToday,
|
||||||
@@ -212,6 +218,7 @@ export class StatsHandler {
|
|||||||
averageDeliveryTime: 0,
|
averageDeliveryTime: 0,
|
||||||
deliveryRate: stats.deliveryRate,
|
deliveryRate: stats.deliveryRate,
|
||||||
bounceRate: stats.bounceRate,
|
bounceRate: stats.bounceRate,
|
||||||
|
timeSeries,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -220,6 +227,11 @@ export class StatsHandler {
|
|||||||
if (sections.dns) {
|
if (sections.dns) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.collectDnsStats().then(stats => {
|
this.collectDnsStats().then(stats => {
|
||||||
|
// Get time-series data from MetricsManager
|
||||||
|
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
||||||
|
? this.opsServerRef.dcRouterRef.metricsManager.getDnsTimeSeries(24)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
metrics.dns = {
|
metrics.dns = {
|
||||||
totalQueries: stats.totalQueries,
|
totalQueries: stats.totalQueries,
|
||||||
cacheHits: stats.cacheHits,
|
cacheHits: stats.cacheHits,
|
||||||
@@ -228,6 +240,7 @@ export class StatsHandler {
|
|||||||
activeDomains: stats.topDomains.length,
|
activeDomains: stats.topDomains.length,
|
||||||
averageResponseTime: 0,
|
averageResponseTime: 0,
|
||||||
queryTypes: stats.queryTypes,
|
queryTypes: stats.queryTypes,
|
||||||
|
timeSeries,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -236,6 +249,19 @@ export class StatsHandler {
|
|||||||
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
||||||
|
// Get recent events from the SecurityLogger singleton
|
||||||
|
const securityLogger = SecurityLogger.getInstance();
|
||||||
|
const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
|
||||||
|
timestamp: evt.timestamp,
|
||||||
|
level: evt.level,
|
||||||
|
type: evt.type,
|
||||||
|
message: evt.message,
|
||||||
|
details: evt.details,
|
||||||
|
ipAddress: evt.ipAddress,
|
||||||
|
domain: evt.domain,
|
||||||
|
success: evt.success,
|
||||||
|
}));
|
||||||
|
|
||||||
metrics.security = {
|
metrics.security = {
|
||||||
blockedIPs: stats.blockedIPs,
|
blockedIPs: stats.blockedIPs,
|
||||||
reputationScores: {},
|
reputationScores: {},
|
||||||
@@ -244,6 +270,7 @@ export class StatsHandler {
|
|||||||
phishingDetected: stats.phishingDetected,
|
phishingDetected: stats.phishingDetected,
|
||||||
authenticationFailures: stats.authFailures,
|
authenticationFailures: stats.authFailures,
|
||||||
suspiciousActivities: stats.totalThreatsBlocked,
|
suspiciousActivities: stats.totalThreatsBlocked,
|
||||||
|
recentEvents,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ export class VlanManager {
|
|||||||
// Cache the result
|
// Cache the result
|
||||||
this.normalizedMacCache.set(mac, normalized);
|
this.normalizedMacCache.set(mac, normalized);
|
||||||
|
|
||||||
|
// Prevent unbounded cache growth
|
||||||
|
if (this.normalizedMacCache.size > 10000) {
|
||||||
|
const iterator = this.normalizedMacCache.keys();
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
this.normalizedMacCache.delete(iterator.next().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
146
ts/readme.md
Normal file
146
ts/readme.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# @serve.zone/dcrouter
|
||||||
|
|
||||||
|
The core DcRouter package — a unified datacenter gateway orchestrator. 🚀
|
||||||
|
|
||||||
|
This is the main entry point for DcRouter. It provides the `DcRouter` class that wires together SmartProxy, smartmta, SmartDNS, SmartRadius, RemoteIngress, and the OpsServer dashboard into a single cohesive service.
|
||||||
|
|
||||||
|
## Issue Reporting and Security
|
||||||
|
|
||||||
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/dcrouter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DcRouter } from '@serve.zone/dcrouter';
|
||||||
|
|
||||||
|
const router = new DcRouter({
|
||||||
|
smartProxyConfig: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'web-app',
|
||||||
|
match: { domains: ['example.com'], ports: [443] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
acme: { email: 'admin@example.com', enabled: true, useProduction: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.start();
|
||||||
|
// OpsServer dashboard at http://localhost:3000
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
await router.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ts/
|
||||||
|
├── index.ts # Main exports (DcRouter, re-exported smartmta types)
|
||||||
|
├── classes.dcrouter.ts # DcRouter orchestrator class + IDcRouterOptions
|
||||||
|
├── classes.cert-provision-scheduler.ts # Per-domain cert backoff scheduler
|
||||||
|
├── classes.storage-cert-manager.ts # SmartAcme cert manager backed by StorageManager
|
||||||
|
├── logger.ts # Structured logging utility
|
||||||
|
├── paths.ts # Centralized data directory paths
|
||||||
|
├── plugins.ts # All dependency imports
|
||||||
|
├── cache/ # Cache database (smartdata + LocalTsmDb)
|
||||||
|
│ ├── classes.cachedb.ts # CacheDb singleton
|
||||||
|
│ ├── classes.cachecleaner.ts # TTL-based cleanup
|
||||||
|
│ └── documents/ # Cached document models
|
||||||
|
├── config/ # Configuration utilities
|
||||||
|
├── errors/ # Error classes and retry logic
|
||||||
|
├── monitoring/ # MetricsManager (SmartMetrics integration)
|
||||||
|
├── opsserver/ # OpsServer dashboard + API handlers
|
||||||
|
│ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
|
||||||
|
│ └── handlers/ # TypedRequest handlers by domain
|
||||||
|
│ ├── admin.handler.ts # Auth (login/logout/verify)
|
||||||
|
│ ├── stats.handler.ts # Statistics + health
|
||||||
|
│ ├── config.handler.ts # Configuration (read-only)
|
||||||
|
│ ├── logs.handler.ts # Log retrieval
|
||||||
|
│ ├── email.handler.ts # Email operations
|
||||||
|
│ ├── certificate.handler.ts # Certificate management
|
||||||
|
│ ├── radius.handler.ts # RADIUS management
|
||||||
|
│ └── remoteingress.handler.ts # Remote ingress edge + token management
|
||||||
|
├── radius/ # RADIUS server integration
|
||||||
|
├── remoteingress/ # Remote ingress hub integration
|
||||||
|
│ ├── classes.remoteingress-manager.ts # Edge CRUD + port derivation
|
||||||
|
│ └── classes.tunnel-manager.ts # Rust hub lifecycle + status tracking
|
||||||
|
├── security/ # Security utilities
|
||||||
|
├── sms/ # SMS integration
|
||||||
|
└── storage/ # StorageManager (filesystem/custom/memory)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Main class
|
||||||
|
export { DcRouter, IDcRouterOptions } from './classes.dcrouter.js';
|
||||||
|
|
||||||
|
// Re-exported from smartmta
|
||||||
|
export { UnifiedEmailServer } from '@push.rocks/smartmta';
|
||||||
|
export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta';
|
||||||
|
|
||||||
|
// RADIUS
|
||||||
|
export { RadiusServer, IRadiusServerConfig } from './radius/index.js';
|
||||||
|
|
||||||
|
// Remote Ingress
|
||||||
|
export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Classes
|
||||||
|
|
||||||
|
### `DcRouter`
|
||||||
|
|
||||||
|
The central orchestrator. Accepts `IDcRouterOptions` and manages the lifecycle of all sub-services:
|
||||||
|
|
||||||
|
| Config Section | Service Started | Package |
|
||||||
|
|----------------|----------------|---------|
|
||||||
|
| `smartProxyConfig` | SmartProxy (HTTP/HTTPS/TCP/SNI) | `@push.rocks/smartproxy` |
|
||||||
|
| `emailConfig` | UnifiedEmailServer (SMTP) | `@push.rocks/smartmta` |
|
||||||
|
| `dnsNsDomains` + `dnsScopes` | DnsServer (UDP + DoH) | `@push.rocks/smartdns` |
|
||||||
|
| `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
|
||||||
|
| `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
|
||||||
|
| `tls` + `dnsChallenge` | SmartAcme (ACME cert provisioning) | `@push.rocks/smartacme` |
|
||||||
|
| `cacheConfig` | CacheDb (embedded MongoDB) | `@push.rocks/smartdata` |
|
||||||
|
| *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
|
||||||
|
| *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
|
||||||
|
|
||||||
|
### `RemoteIngressManager`
|
||||||
|
|
||||||
|
Manages CRUD for remote ingress edge registrations. Persists edges via StorageManager. Provides port derivation from routes tagged with `remoteIngress.enabled`.
|
||||||
|
|
||||||
|
### `TunnelManager`
|
||||||
|
|
||||||
|
Manages the Rust-based RemoteIngressHub lifecycle. Syncs allowed edges, tracks connection status, and exposes edge statuses (connected, publicIp, activeTunnels, lastHeartbeat).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
@@ -47,6 +47,11 @@ export class RemoteIngressManager {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
||||||
if (edge) {
|
if (edge) {
|
||||||
|
// Migration: old edges without autoDerivePorts default to true
|
||||||
|
if ((edge as any).autoDerivePorts === undefined) {
|
||||||
|
edge.autoDerivePorts = true;
|
||||||
|
await this.storageManager.setJSON(key, edge);
|
||||||
|
}
|
||||||
this.edges.set(edge.id, edge);
|
this.edges.set(edge.id, edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,13 +96,28 @@ export class RemoteIngressManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the effective listen ports for an edge.
|
* Get the effective listen ports for an edge.
|
||||||
* Returns manual listenPorts if non-empty, otherwise derives ports from tagged routes.
|
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
||||||
*/
|
*/
|
||||||
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
|
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
|
||||||
if (edge.listenPorts && edge.listenPorts.length > 0) {
|
const manualPorts = edge.listenPorts || [];
|
||||||
return edge.listenPorts;
|
const shouldDerive = edge.autoDerivePorts !== false;
|
||||||
|
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
|
||||||
|
const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags);
|
||||||
|
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
return this.derivePortsForEdge(edge.id, edge.tags);
|
|
||||||
|
/**
|
||||||
|
* Get manual and derived port breakdown for an edge (used in API responses).
|
||||||
|
* Derived ports exclude any ports already present in the manual list.
|
||||||
|
*/
|
||||||
|
public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } {
|
||||||
|
const manual = edge.listenPorts || [];
|
||||||
|
const shouldDerive = edge.autoDerivePorts !== false;
|
||||||
|
if (!shouldDerive) return { manual, derived: [] };
|
||||||
|
const manualSet = new Set(manual);
|
||||||
|
const allDerived = this.derivePortsForEdge(edge.id, edge.tags);
|
||||||
|
const derived = allDerived.filter((p) => !manualSet.has(p));
|
||||||
|
return { manual, derived };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +127,7 @@ export class RemoteIngressManager {
|
|||||||
name: string,
|
name: string,
|
||||||
listenPorts: number[] = [],
|
listenPorts: number[] = [],
|
||||||
tags?: string[],
|
tags?: string[],
|
||||||
|
autoDerivePorts: boolean = true,
|
||||||
): Promise<IRemoteIngress> {
|
): Promise<IRemoteIngress> {
|
||||||
const id = plugins.uuid.v4();
|
const id = plugins.uuid.v4();
|
||||||
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||||
@@ -118,6 +139,7 @@ export class RemoteIngressManager {
|
|||||||
secret,
|
secret,
|
||||||
listenPorts,
|
listenPorts,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
autoDerivePorts,
|
||||||
tags: tags || [],
|
tags: tags || [],
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -150,6 +172,7 @@ export class RemoteIngressManager {
|
|||||||
updates: {
|
updates: {
|
||||||
name?: string;
|
name?: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
},
|
},
|
||||||
@@ -161,6 +184,7 @@ export class RemoteIngressManager {
|
|||||||
|
|
||||||
if (updates.name !== undefined) edge.name = updates.name;
|
if (updates.name !== undefined) edge.name = updates.name;
|
||||||
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||||
|
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
||||||
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||||
if (updates.tags !== undefined) edge.tags = updates.tags;
|
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||||
edge.updatedAt = Date.now();
|
edge.updatedAt = Date.now();
|
||||||
@@ -218,11 +242,15 @@ export class RemoteIngressManager {
|
|||||||
/**
|
/**
|
||||||
* Get the list of allowed edges (enabled only) for the Rust hub.
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||||
*/
|
*/
|
||||||
public getAllowedEdges(): Array<{ id: string; secret: string }> {
|
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[] }> {
|
||||||
const result: Array<{ id: string; secret: string }> = [];
|
const result: Array<{ id: string; secret: string; listenPorts: number[] }> = [];
|
||||||
for (const edge of this.edges.values()) {
|
for (const edge of this.edges.values()) {
|
||||||
if (edge.enabled) {
|
if (edge.enabled) {
|
||||||
result.push({ id: edge.id, secret: edge.secret });
|
result.push({
|
||||||
|
id: edge.id,
|
||||||
|
secret: edge.secret,
|
||||||
|
listenPorts: this.getEffectiveListenPorts(edge),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ export interface IRemoteIngress {
|
|||||||
secret: string;
|
secret: string;
|
||||||
listenPorts: number[];
|
listenPorts: number[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||||
|
autoDerivePorts: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
/** Effective ports (union of manual + derived) — only present in API responses. */
|
||||||
|
effectiveListenPorts?: number[];
|
||||||
|
/** Ports explicitly set by the user — only present in API responses. */
|
||||||
|
manualPorts?: number[];
|
||||||
|
/** Ports auto-derived from route configs — only present in API responses. */
|
||||||
|
derivedPorts?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export interface IServerStats {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITimeSeriesPoint {
|
||||||
|
timestamp: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IEmailStats {
|
export interface IEmailStats {
|
||||||
sent: number;
|
sent: number;
|
||||||
received: number;
|
received: number;
|
||||||
@@ -35,6 +40,11 @@ export interface IEmailStats {
|
|||||||
averageDeliveryTime: number;
|
averageDeliveryTime: number;
|
||||||
deliveryRate: number;
|
deliveryRate: number;
|
||||||
bounceRate: number;
|
bounceRate: number;
|
||||||
|
timeSeries?: {
|
||||||
|
sent: ITimeSeriesPoint[];
|
||||||
|
received: ITimeSeriesPoint[];
|
||||||
|
failed: ITimeSeriesPoint[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDnsStats {
|
export interface IDnsStats {
|
||||||
@@ -47,6 +57,9 @@ export interface IDnsStats {
|
|||||||
queryTypes: {
|
queryTypes: {
|
||||||
[key: string]: number;
|
[key: string]: number;
|
||||||
};
|
};
|
||||||
|
timeSeries?: {
|
||||||
|
queries: ITimeSeriesPoint[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRateLimitInfo {
|
export interface IRateLimitInfo {
|
||||||
@@ -58,6 +71,17 @@ export interface IRateLimitInfo {
|
|||||||
blocked: boolean;
|
blocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISecurityEvent {
|
||||||
|
timestamp: number;
|
||||||
|
level: string;
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
details?: any;
|
||||||
|
ipAddress?: string;
|
||||||
|
domain?: string;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISecurityMetrics {
|
export interface ISecurityMetrics {
|
||||||
blockedIPs: string[];
|
blockedIPs: string[];
|
||||||
reputationScores: {
|
reputationScores: {
|
||||||
@@ -68,6 +92,7 @@ export interface ISecurityMetrics {
|
|||||||
phishingDetected: number;
|
phishingDetected: number;
|
||||||
authenticationFailures: number;
|
authenticationFailures: number;
|
||||||
suspiciousActivities: number;
|
suspiciousActivities: number;
|
||||||
|
recentEvents?: ISecurityEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILogEntry {
|
export interface ILogEntry {
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ interface IIdentity {
|
|||||||
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
||||||
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
||||||
|
|
||||||
|
#### Remote Ingress Interfaces
|
||||||
|
| Interface | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `IRemoteIngress` | Edge registration: id, name, secret, listenPorts, enabled, autoDerivePorts, tags |
|
||||||
|
| `IRemoteIngressStatus` | Runtime status: connected, publicIp, activeTunnels, lastHeartbeat |
|
||||||
|
| `IRouteRemoteIngress` | Route-level config: enabled flag and optional edgeFilter |
|
||||||
|
| `IDcRouterRouteConfig` | Extended SmartProxy route config with optional `remoteIngress` property |
|
||||||
|
|
||||||
### Request Interfaces (`requests`)
|
### Request Interfaces (`requests`)
|
||||||
|
|
||||||
TypedRequest interfaces for the OpsServer API, organized by domain:
|
TypedRequest interfaces for the OpsServer API, organized by domain:
|
||||||
@@ -134,6 +142,9 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
|||||||
| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status |
|
| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status |
|
||||||
| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) |
|
| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) |
|
||||||
| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) |
|
| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) |
|
||||||
|
| `IReq_ImportCertificate` | `importCertificate` | Import a certificate |
|
||||||
|
| `IReq_ExportCertificate` | `exportCertificate` | Export a certificate |
|
||||||
|
| `IReq_DeleteCertificate` | `deleteCertificate` | Delete a certificate |
|
||||||
|
|
||||||
#### Certificate Types
|
#### Certificate Types
|
||||||
```typescript
|
```typescript
|
||||||
@@ -159,6 +170,17 @@ interface ICertificateInfo {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 🌍 Remote Ingress
|
||||||
|
| Interface | Method | Description |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| `IReq_CreateRemoteIngress` | `createRemoteIngress` | Register a new edge node |
|
||||||
|
| `IReq_DeleteRemoteIngress` | `deleteRemoteIngress` | Remove an edge registration |
|
||||||
|
| `IReq_UpdateRemoteIngress` | `updateRemoteIngress` | Update edge settings |
|
||||||
|
| `IReq_RegenerateRemoteIngressSecret` | `regenerateRemoteIngressSecret` | Issue a new secret |
|
||||||
|
| `IReq_GetRemoteIngresses` | `getRemoteIngresses` | List all edge registrations |
|
||||||
|
| `IReq_GetRemoteIngressStatus` | `getRemoteIngressStatus` | Runtime status of all edges |
|
||||||
|
| `IReq_GetRemoteIngressConnectionToken` | `getRemoteIngressConnectionToken` | Generate a connection token |
|
||||||
|
|
||||||
#### 📡 RADIUS
|
#### 📡 RADIUS
|
||||||
| Interface | Method | Description |
|
| Interface | Method | Description |
|
||||||
|-----------|--------|-------------|
|
|-----------|--------|-------------|
|
||||||
@@ -183,7 +205,7 @@ import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
|||||||
// 1. Login
|
// 1. Login
|
||||||
const loginClient = new typedrequest.TypedRequest<requests.IReq_AdminLoginWithUsernameAndPassword>(
|
const loginClient = new typedrequest.TypedRequest<requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'https://your-dcrouter:3000/typedrequest',
|
'https://your-dcrouter:3000/typedrequest',
|
||||||
'adminLogin'
|
'adminLoginWithUsernameAndPassword'
|
||||||
);
|
);
|
||||||
|
|
||||||
const loginResponse = await loginClient.fire({
|
const loginResponse = await loginClient.fire({
|
||||||
@@ -199,10 +221,8 @@ const metricsClient = new typedrequest.TypedRequest<requests.IReq_GetCombinedMet
|
|||||||
);
|
);
|
||||||
|
|
||||||
const metrics = await metricsClient.fire({ identity });
|
const metrics = await metricsClient.fire({ identity });
|
||||||
console.log('Server:', metrics.serverStats);
|
console.log('Server:', metrics.metrics.server);
|
||||||
console.log('Email:', metrics.emailStats);
|
console.log('Email:', metrics.metrics.email);
|
||||||
console.log('DNS:', metrics.dnsStats);
|
|
||||||
console.log('Security:', metrics.securityMetrics);
|
|
||||||
|
|
||||||
// 3. Check certificate status
|
// 3. Check certificate status
|
||||||
const certClient = new typedrequest.TypedRequest<requests.IReq_GetCertificateOverview>(
|
const certClient = new typedrequest.TypedRequest<requests.IReq_GetCertificateOverview>(
|
||||||
@@ -213,14 +233,23 @@ const certClient = new typedrequest.TypedRequest<requests.IReq_GetCertificateOve
|
|||||||
const certs = await certClient.fire({ identity });
|
const certs = await certClient.fire({ identity });
|
||||||
console.log(`Certificates: ${certs.summary.valid} valid, ${certs.summary.failed} failed`);
|
console.log(`Certificates: ${certs.summary.valid} valid, ${certs.summary.failed} failed`);
|
||||||
|
|
||||||
// 4. Check email queues
|
// 4. List remote ingress edges
|
||||||
const queueClient = new typedrequest.TypedRequest<requests.IReq_GetQueuedEmails>(
|
const edgesClient = new typedrequest.TypedRequest<requests.IReq_GetRemoteIngresses>(
|
||||||
'https://your-dcrouter:3000/typedrequest',
|
'https://your-dcrouter:3000/typedrequest',
|
||||||
'getQueuedEmails'
|
'getRemoteIngresses'
|
||||||
);
|
);
|
||||||
|
|
||||||
const queued = await queueClient.fire({ identity });
|
const edges = await edgesClient.fire({ identity });
|
||||||
console.log('Queued emails:', queued.emails.length);
|
console.log('Registered edges:', edges.edges.length);
|
||||||
|
|
||||||
|
// 5. Generate a connection token for an edge
|
||||||
|
const tokenClient = new typedrequest.TypedRequest<requests.IReq_GetRemoteIngressConnectionToken>(
|
||||||
|
'https://your-dcrouter:3000/typedrequest',
|
||||||
|
'getRemoteIngressConnectionToken'
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenResponse = await tokenClient.fire({ identity, edgeId: edges.edges[0].id });
|
||||||
|
console.log('Connection token:', tokenResponse.token);
|
||||||
```
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|||||||
@@ -74,3 +74,68 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
|
|||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a certificate by domain
|
||||||
|
export interface IReq_DeleteCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteCertificate
|
||||||
|
> {
|
||||||
|
method: 'deleteCertificate';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a certificate as ICert JSON
|
||||||
|
export interface IReq_ExportCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ExportCertificate
|
||||||
|
> {
|
||||||
|
method: 'exportCertificate';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
cert?: {
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import a certificate from ICert JSON
|
||||||
|
export interface IReq_ImportCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ImportCertificate
|
||||||
|
> {
|
||||||
|
method: 'importCertificate';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
cert: {
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
identity?: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
name: string;
|
name: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -57,6 +58,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
@@ -115,3 +117,24 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa
|
|||||||
statuses: IRemoteIngressStatus[];
|
statuses: IRemoteIngressStatus[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a connection token for a remote ingress edge.
|
||||||
|
* The token is a single opaque base64url string that encodes hubHost, hubPort, edgeId, and secret.
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRemoteIngressConnectionToken
|
||||||
|
> {
|
||||||
|
method: 'getRemoteIngressConnectionToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
edgeId: string;
|
||||||
|
hubHost?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
token?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.6.0',
|
version: '7.1.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export interface IRemoteIngressState {
|
|||||||
edges: interfaces.data.IRemoteIngress[];
|
edges: interfaces.data.IRemoteIngress[];
|
||||||
statuses: interfaces.data.IRemoteIngressStatus[];
|
statuses: interfaces.data.IRemoteIngressStatus[];
|
||||||
selectedEdgeId: string | null;
|
selectedEdgeId: string | null;
|
||||||
newEdgeSecret: string | null;
|
newEdgeId: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
@@ -212,7 +212,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
|
|||||||
edges: [],
|
edges: [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
selectedEdgeId: null,
|
selectedEdgeId: null,
|
||||||
newEdgeSecret: null,
|
newEdgeId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
@@ -780,6 +780,92 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const deleteCertificateAction = 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_DeleteCertificate
|
||||||
|
>('/typedrequest', 'deleteCertificate');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-fetch overview after deletion
|
||||||
|
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete certificate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const importCertificateAction = certificateStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
}>(
|
||||||
|
async (statePartArg, cert) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ImportCertificate
|
||||||
|
>('/typedrequest', 'importCertificate');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
cert,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-fetch overview after import
|
||||||
|
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to import certificate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function fetchCertificateExport(domain: string) {
|
||||||
|
const context = getActionContext();
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ExportCertificate
|
||||||
|
>('/typedrequest', 'exportCertificate');
|
||||||
|
|
||||||
|
return request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Remote Ingress Standalone Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export async function fetchConnectionToken(edgeId: string) {
|
||||||
|
const context = getActionContext();
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetRemoteIngressConnectionToken
|
||||||
|
>('/typedrequest', 'getRemoteIngressConnectionToken');
|
||||||
|
return request.fire({ identity: context.identity, edgeId });
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Remote Ingress Actions
|
// Remote Ingress Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -821,7 +907,8 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|||||||
|
|
||||||
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
name: string;
|
name: string;
|
||||||
listenPorts: number[];
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -836,15 +923,17 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
name: dataArg.name,
|
name: dataArg.name,
|
||||||
listenPorts: dataArg.listenPorts,
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
tags: dataArg.tags,
|
tags: dataArg.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Refresh the list and store the new secret for display
|
// Refresh the list
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...statePartArg.getState(),
|
...statePartArg.getState(),
|
||||||
newEdgeSecret: response.edge.secret,
|
newEdgeId: response.edge.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,6 +972,40 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateRemoteIngress
|
||||||
|
>('/typedrequest', 'updateRemoteIngress');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
name: dataArg.name,
|
||||||
|
listenPorts: dataArg.listenPorts,
|
||||||
|
autoDerivePorts: dataArg.autoDerivePorts,
|
||||||
|
tags: dataArg.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||||
async (statePartArg, edgeId) => {
|
async (statePartArg, edgeId) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -901,7 +1024,7 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
newEdgeSecret: response.secret,
|
newEdgeId: edgeId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,15 +1038,43 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
|
||||||
async (statePartArg) => {
|
async (statePartArg) => {
|
||||||
return {
|
return {
|
||||||
...statePartArg.getState(),
|
...statePartArg.getState(),
|
||||||
newEdgeSecret: null,
|
newEdgeId: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateRemoteIngress
|
||||||
|
>('/typedrequest', 'updateRemoteIngress');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Combined refresh action for efficient polling
|
// Combined refresh action for efficient polling
|
||||||
async function dispatchCombinedRefreshAction() {
|
async function dispatchCombinedRefreshAction() {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Total Certificates',
|
title: 'Total Certificates',
|
||||||
value: summary.total,
|
value: summary.total,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'shieldHalved',
|
icon: 'lucide:ShieldHalf',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -183,7 +183,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Valid',
|
title: 'Valid',
|
||||||
value: summary.valid,
|
value: summary.valid,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'check',
|
icon: 'lucide:Check',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -191,7 +191,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Expiring Soon',
|
title: 'Expiring Soon',
|
||||||
value: summary.expiring,
|
value: summary.expiring,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clock',
|
icon: 'lucide:Clock',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -199,7 +199,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
title: 'Failed / Expired',
|
title: 'Failed / Expired',
|
||||||
value: summary.failed + summary.expired,
|
value: summary.failed + summary.expired,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -211,7 +211,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Refresh',
|
name: 'Refresh',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await appstate.certificateStatePart.dispatchAction(
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
appstate.fetchCertificateOverviewAction,
|
appstate.fetchCertificateOverviewAction,
|
||||||
@@ -241,9 +241,64 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
: '',
|
: '',
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Import Certificate',
|
||||||
|
iconName: 'lucide:upload',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Import Certificate',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-fileupload
|
||||||
|
key="certJsonFile"
|
||||||
|
label="Certificate JSON (.tsclass.cert.json)"
|
||||||
|
accept=".json"
|
||||||
|
.multiple=${false}
|
||||||
|
required
|
||||||
|
></dees-input-fileupload>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Import',
|
||||||
|
iconName: 'lucide:upload',
|
||||||
|
action: async (modal) => {
|
||||||
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
try {
|
||||||
|
const form = modal.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
const files = formData.certJsonFile;
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
DeesToast.show({ message: 'Please select a JSON file.', type: 'warning', duration: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const text = await file.text();
|
||||||
|
const cert = JSON.parse(text);
|
||||||
|
if (!cert.domainName || !cert.publicKey || !cert.privateKey) {
|
||||||
|
DeesToast.show({ message: 'Invalid cert JSON: missing domainName, publicKey, or privateKey.', type: 'error', duration: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
|
appstate.importCertificateAction,
|
||||||
|
cert,
|
||||||
|
);
|
||||||
|
DeesToast.show({ message: `Certificate imported for ${cert.domainName}`, type: 'success', duration: 3000 });
|
||||||
|
modal.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
DeesToast.show({ message: `Import failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Reprovision',
|
name: 'Reprovision',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
type: ['inRow'],
|
type: ['inRow'],
|
||||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
@@ -268,9 +323,66 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Export',
|
||||||
|
iconName: 'lucide:download',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
const cert = actionData.item;
|
||||||
|
try {
|
||||||
|
const response = await appstate.fetchCertificateExport(cert.domain);
|
||||||
|
if (response.success && response.cert) {
|
||||||
|
const safeDomain = cert.domain.replace(/\*/g, '_wildcard');
|
||||||
|
this.downloadJsonFile(`${safeDomain}.tsclass.cert.json`, response.cert);
|
||||||
|
DeesToast.show({ message: `Certificate exported for ${cert.domain}`, type: 'success', duration: 3000 });
|
||||||
|
} else {
|
||||||
|
DeesToast.show({ message: response.message || 'Export failed', type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
DeesToast.show({ message: `Export failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
type: ['inRow', 'contextmenu'],
|
||||||
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
|
const cert = actionData.item;
|
||||||
|
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Delete Certificate: ${cert.domain}`,
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px; font-size: 14px;">
|
||||||
|
<p>Are you sure you want to delete the certificate data for <strong>${cert.domain}</strong>?</p>
|
||||||
|
<p style="color: #f59e0b; margin-top: 12px;">Note: The certificate may remain in proxy memory until the next restart or reprovisioning.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
action: async (modal) => {
|
||||||
|
try {
|
||||||
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
|
appstate.deleteCertificateAction,
|
||||||
|
cert.domain,
|
||||||
|
);
|
||||||
|
DeesToast.show({ message: `Certificate deleted for ${cert.domain}`, type: 'success', duration: 3000 });
|
||||||
|
modal.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
DeesToast.show({ message: `Delete failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'fa:magnifyingGlass',
|
||||||
type: ['doubleClick', 'contextmenu'],
|
type: ['doubleClick', 'contextmenu'],
|
||||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
@@ -289,7 +401,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Domain',
|
name: 'Copy Domain',
|
||||||
iconName: 'copy',
|
iconName: 'lucide:Copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(cert.domain);
|
await navigator.clipboard.writeText(cert.domain);
|
||||||
},
|
},
|
||||||
@@ -309,6 +421,19 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private downloadJsonFile(filename: string, data: any): void {
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
private renderRoutePills(routeNames: string[]): TemplateResult {
|
private renderRoutePills(routeNames: string[]): TemplateResult {
|
||||||
const maxShow = 3;
|
const maxShow = 3;
|
||||||
const visible = routeNames.slice(0, maxShow);
|
const visible = routeNames.slice(0, maxShow);
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
filters: {},
|
filters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor filterLevel: string | undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor filterCategory: string | undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor filterLimit: number = 100;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subscription = appstate.logStatePart
|
const subscription = appstate.logStatePart
|
||||||
@@ -174,29 +183,43 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
// Auto-fetch logs when the view mounts
|
||||||
|
this.fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
private async fetchLogs() {
|
private async fetchLogs() {
|
||||||
const filters = this.getActiveFilters();
|
|
||||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
||||||
limit: filters.limit || 100,
|
limit: this.filterLimit,
|
||||||
level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||||
category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateFilter(type: string, value: string) {
|
private updateFilter(type: string, value: string) {
|
||||||
if (value === 'all') {
|
const resolved = value === 'all' ? undefined : value;
|
||||||
value = undefined;
|
|
||||||
|
switch (type) {
|
||||||
|
case 'level':
|
||||||
|
this.filterLevel = resolved;
|
||||||
|
break;
|
||||||
|
case 'category':
|
||||||
|
this.filterCategory = resolved;
|
||||||
|
break;
|
||||||
|
case 'limit':
|
||||||
|
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update filters then fetch logs
|
|
||||||
this.fetchLogs();
|
this.fetchLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getActiveFilters() {
|
private getActiveFilters() {
|
||||||
return {
|
return {
|
||||||
level: this.logState.filters.level?.[0],
|
level: this.filterLevel,
|
||||||
category: this.logState.filters.category?.[0],
|
category: this.filterCategory,
|
||||||
limit: 100,
|
limit: this.filterLimit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'fa:magnifyingGlass',
|
||||||
type: ['inRow', 'doubleClick', 'contextmenu'],
|
type: ['inRow', 'doubleClick', 'contextmenu'],
|
||||||
actionFunc: async (actionData) => {
|
actionFunc: async (actionData) => {
|
||||||
await this.showRequestDetails(actionData.item);
|
await this.showRequestDetails(actionData.item);
|
||||||
@@ -336,7 +336,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Request ID',
|
name: 'Copy Request ID',
|
||||||
iconName: 'copy',
|
iconName: 'lucide:Copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(request.id);
|
await navigator.clipboard.writeText(request.id);
|
||||||
}
|
}
|
||||||
@@ -429,13 +429,13 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
title: 'Active Connections',
|
title: 'Active Connections',
|
||||||
value: activeConnections,
|
value: activeConnections,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'plug',
|
icon: 'lucide:Plug',
|
||||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||||
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
iconName: 'magnifyingGlass',
|
iconName: 'fa:magnifyingGlass',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -446,7 +446,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
title: 'Requests/sec',
|
title: 'Requests/sec',
|
||||||
value: reqPerSec,
|
value: reqPerSec,
|
||||||
type: 'trend',
|
type: 'trend',
|
||||||
icon: 'chartLine',
|
icon: 'lucide:ChartLine',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
trendData: trendData,
|
trendData: trendData,
|
||||||
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
||||||
@@ -457,7 +457,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
value: this.formatBitsPerSecond(throughput.in),
|
value: this.formatBitsPerSecond(throughput.in),
|
||||||
unit: '',
|
unit: '',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'download',
|
icon: 'lucide:Download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -467,7 +467,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
value: this.formatBitsPerSecond(throughput.out),
|
value: this.formatBitsPerSecond(throughput.out),
|
||||||
unit: '',
|
unit: '',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'upload',
|
icon: 'lucide:Upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -480,7 +480,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Export Data',
|
name: 'Export Data',
|
||||||
iconName: 'fileExport',
|
iconName: 'lucide:FileOutput',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
console.log('Export feature coming soon');
|
console.log('Export feature coming soon');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor logState: appstate.ILogState = {
|
||||||
|
recentLogs: [],
|
||||||
|
isStreaming: false,
|
||||||
|
filters: {},
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subscription = appstate.statsStatePart
|
const statsSub = appstate.statsStatePart
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
.subscribe((statsState) => {
|
.subscribe((statsState) => {
|
||||||
this.statsState = statsState;
|
this.statsState = statsState;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(subscription);
|
this.rxSubscriptions.push(statsSub);
|
||||||
|
|
||||||
|
const logSub = appstate.logStatePart
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((logState) => {
|
||||||
|
this.logState = logState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(logSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
// Ensure logs are fetched for the overview charts
|
||||||
|
if (this.logState.recentLogs.length === 0) {
|
||||||
|
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
${this.renderDnsStats()}
|
${this.renderDnsStats()}
|
||||||
|
|
||||||
<div class="chartGrid">
|
<div class="chartGrid">
|
||||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
<dees-chart-area
|
||||||
<dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area>
|
.label=${'Email Traffic (24h)'}
|
||||||
<dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log>
|
.series=${this.getEmailTrafficSeries()}
|
||||||
<dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log>
|
.yAxisFormatter=${(val: number) => `${val}`}
|
||||||
|
></dees-chart-area>
|
||||||
|
<dees-chart-area
|
||||||
|
.label=${'DNS Queries (24h)'}
|
||||||
|
.series=${this.getDnsQuerySeries()}
|
||||||
|
.yAxisFormatter=${(val: number) => `${val}`}
|
||||||
|
></dees-chart-area>
|
||||||
|
<dees-chart-log
|
||||||
|
.label=${'Recent Events'}
|
||||||
|
.logEntries=${this.getRecentEventEntries()}
|
||||||
|
></dees-chart-log>
|
||||||
|
<dees-chart-log
|
||||||
|
.label=${'Security Alerts'}
|
||||||
|
.logEntries=${this.getSecurityAlertEntries()}
|
||||||
|
></dees-chart-log>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
@@ -163,7 +199,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Server Status',
|
title: 'Server Status',
|
||||||
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'server',
|
icon: 'lucide:Server',
|
||||||
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
||||||
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
||||||
},
|
},
|
||||||
@@ -172,7 +208,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Active Connections',
|
title: 'Active Connections',
|
||||||
value: this.statsState.serverStats.activeConnections,
|
value: this.statsState.serverStats.activeConnections,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'networkWired',
|
icon: 'lucide:Network',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
||||||
},
|
},
|
||||||
@@ -181,7 +217,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Throughput In',
|
title: 'Throughput In',
|
||||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
|
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'download',
|
icon: 'lucide:Download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
|
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -190,7 +226,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Throughput Out',
|
title: 'Throughput Out',
|
||||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
|
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
icon: 'upload',
|
icon: 'lucide:Upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
|
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
|
||||||
},
|
},
|
||||||
@@ -199,7 +235,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'CPU Usage',
|
title: 'CPU Usage',
|
||||||
value: cpuUsage,
|
value: cpuUsage,
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
icon: 'microchip',
|
icon: 'lucide:Cpu',
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -215,7 +251,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Memory Usage',
|
title: 'Memory Usage',
|
||||||
value: memoryUsage,
|
value: memoryUsage,
|
||||||
type: 'percentage',
|
type: 'percentage',
|
||||||
icon: 'memory',
|
icon: 'lucide:MemoryStick',
|
||||||
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
||||||
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
|
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
|
||||||
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
|
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
|
||||||
@@ -229,7 +265,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
.gridActions=${[
|
.gridActions=${[
|
||||||
{
|
{
|
||||||
name: 'Refresh',
|
name: 'Refresh',
|
||||||
iconName: 'arrowsRotate',
|
iconName: 'lucide:RefreshCw',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||||
},
|
},
|
||||||
@@ -251,7 +287,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Emails Sent',
|
title: 'Emails Sent',
|
||||||
value: this.statsState.emailStats.sent,
|
value: this.statsState.emailStats.sent,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'paperPlane',
|
icon: 'lucide:Send',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
||||||
},
|
},
|
||||||
@@ -260,7 +296,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Emails Received',
|
title: 'Emails Received',
|
||||||
value: this.statsState.emailStats.received,
|
value: this.statsState.emailStats.received,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'envelope',
|
icon: 'lucide:Mail',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -268,7 +304,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Queued',
|
title: 'Queued',
|
||||||
value: this.statsState.emailStats.queued,
|
value: this.statsState.emailStats.queued,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clock',
|
icon: 'lucide:Clock',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
description: 'Pending delivery',
|
description: 'Pending delivery',
|
||||||
},
|
},
|
||||||
@@ -277,7 +313,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Failed',
|
title: 'Failed',
|
||||||
value: this.statsState.emailStats.failed,
|
value: this.statsState.emailStats.failed,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
||||||
},
|
},
|
||||||
@@ -300,7 +336,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'DNS Queries',
|
title: 'DNS Queries',
|
||||||
value: this.statsState.dnsStats.totalQueries,
|
value: this.statsState.dnsStats.totalQueries,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'globe',
|
icon: 'lucide:Globe',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
description: 'Total queries handled',
|
description: 'Total queries handled',
|
||||||
},
|
},
|
||||||
@@ -309,7 +345,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Cache Hit Rate',
|
title: 'Cache Hit Rate',
|
||||||
value: cacheHitRate,
|
value: cacheHitRate,
|
||||||
type: 'percentage',
|
type: 'percentage',
|
||||||
icon: 'database',
|
icon: 'lucide:Database',
|
||||||
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
||||||
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
||||||
},
|
},
|
||||||
@@ -318,7 +354,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
title: 'Active Domains',
|
title: 'Active Domains',
|
||||||
value: this.statsState.dnsStats.activeDomains,
|
value: this.statsState.dnsStats.activeDomains,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'sitemap',
|
icon: 'lucide:Network',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -327,7 +363,7 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'clockRotateLeft',
|
icon: 'lucide:History',
|
||||||
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -337,4 +373,42 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Chart data helpers ---
|
||||||
|
|
||||||
|
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||||
|
return this.logState.recentLogs.map((log) => ({
|
||||||
|
timestamp: new Date(log.timestamp).toISOString(),
|
||||||
|
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
||||||
|
message: log.message,
|
||||||
|
source: log.category,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||||
|
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
|
||||||
|
return events.map((evt: any) => ({
|
||||||
|
timestamp: new Date(evt.timestamp).toISOString(),
|
||||||
|
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
|
||||||
|
message: evt.message,
|
||||||
|
source: evt.type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
||||||
|
const ts = this.statsState.emailStats?.timeSeries;
|
||||||
|
if (!ts) return [];
|
||||||
|
return [
|
||||||
|
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||||
|
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
||||||
|
const ts = this.statsState.dnsStats?.timeSeries;
|
||||||
|
if (!ts) return [];
|
||||||
|
return [
|
||||||
|
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +114,17 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portBadge.manual {
|
||||||
|
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||||
|
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.portBadge.derived {
|
||||||
|
background: ${cssManager.bdTheme('#ecfdf5', '#022c22')};
|
||||||
|
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
||||||
|
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -165,13 +176,39 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Remote Ingress</ops-sectionheading>
|
<ops-sectionheading>Remote Ingress</ops-sectionheading>
|
||||||
|
|
||||||
${this.riState.newEdgeSecret ? html`
|
${this.riState.newEdgeId ? html`
|
||||||
<div class="secretDialog">
|
<div class="secretDialog">
|
||||||
<strong>Edge Secret (copy now - shown only once):</strong>
|
<strong>Edge created successfully!</strong>
|
||||||
<code>${this.riState.newEdgeSecret}</code>
|
<div class="warning">Copy the connection token now. Use it with edge.start({ token: '...' }).</div>
|
||||||
<div class="warning">This secret will not be shown again. Save it securely.</div>
|
|
||||||
<dees-button
|
<dees-button
|
||||||
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)}
|
@click=${async () => {
|
||||||
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
try {
|
||||||
|
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId);
|
||||||
|
if (response.success && response.token) {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
await navigator.clipboard.writeText(response.token);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = response.token;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
DeesToast.show({ message: 'Connection token copied!', type: 'success', duration: 3000 });
|
||||||
|
} else {
|
||||||
|
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>Copy Connection Token</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeIdAction, null)}
|
||||||
>Dismiss</dees-button>
|
>Dismiss</dees-button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -187,7 +224,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
name: edge.name,
|
name: edge.name,
|
||||||
status: this.getEdgeStatusHtml(edge),
|
status: this.getEdgeStatusHtml(edge),
|
||||||
publicIp: this.getEdgePublicIp(edge.id),
|
publicIp: this.getEdgePublicIp(edge.id),
|
||||||
ports: this.getPortsHtml(edge.listenPorts),
|
ports: this.getPortsHtml(edge),
|
||||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||||
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
||||||
})}
|
})}
|
||||||
@@ -198,53 +235,181 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
type: ['header'],
|
type: ['header'],
|
||||||
actionFunc: async () => {
|
actionFunc: async () => {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
const result = await DeesModal.createAndShow({
|
const modal = await DeesModal.createAndShow({
|
||||||
heading: 'Create Edge Node',
|
heading: 'Create Edge Node',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||||
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
|
<dees-input-text .key=${'listenPorts'} .label=${'Additional Manual Ports (comma-separated, optional)'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
||||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [],
|
menuOptions: [
|
||||||
});
|
{
|
||||||
if (result) {
|
name: 'Cancel',
|
||||||
const formData = result as any;
|
iconName: 'lucide:x',
|
||||||
const ports = (formData.name ? formData.listenPorts : '443')
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
.split(',')
|
},
|
||||||
.map((p: string) => parseInt(p.trim(), 10))
|
{
|
||||||
.filter((p: number) => !isNaN(p));
|
name: 'Create',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
const name = formData.name;
|
||||||
|
if (!name) return;
|
||||||
|
const portsStr = formData.listenPorts?.trim();
|
||||||
|
const listenPorts = portsStr
|
||||||
|
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||||
|
: undefined;
|
||||||
|
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||||
const tags = formData.tags
|
const tags = formData.tags
|
||||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||||
: undefined;
|
: undefined;
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
appstate.createRemoteIngressAction,
|
appstate.createRemoteIngressAction,
|
||||||
|
{ name, listenPorts, autoDerivePorts, tags },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: formData.name,
|
name: 'Enable',
|
||||||
listenPorts: ports,
|
iconName: 'lucide:play',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.toggleRemoteIngressAction,
|
||||||
|
{ id: edge.id, enabled: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disable',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.toggleRemoteIngressAction,
|
||||||
|
{ id: edge.id, enabled: false },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
iconName: 'lucide:pencil',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Edit Edge: ${edge.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports (comma-separated)'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated)'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
iconName: 'lucide:check',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
const portsStr = formData.listenPorts?.trim();
|
||||||
|
const listenPorts = portsStr
|
||||||
|
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||||
|
: [];
|
||||||
|
const autoDerivePorts = formData.autoDerivePorts !== false;
|
||||||
|
const tags = formData.tags
|
||||||
|
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.updateRemoteIngressAction,
|
||||||
|
{
|
||||||
|
id: edge.id,
|
||||||
|
name: formData.name || edge.name,
|
||||||
|
listenPorts,
|
||||||
|
autoDerivePorts,
|
||||||
tags,
|
tags,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Regenerate Secret',
|
name: 'Regenerate Secret',
|
||||||
iconName: 'lucide:key',
|
iconName: 'lucide:key',
|
||||||
type: ['row'],
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
appstate.regenerateRemoteIngressSecretAction,
|
appstate.regenerateRemoteIngressSecretAction,
|
||||||
edge.id,
|
edge.id,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Copy Token',
|
||||||
|
iconName: 'lucide:ClipboardCopy',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
try {
|
||||||
|
const response = await appstate.fetchConnectionToken(edge.id);
|
||||||
|
if (response.success && response.token) {
|
||||||
|
// Use clipboard API with fallback for non-HTTPS contexts
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
await navigator.clipboard.writeText(response.token);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = response.token;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
DeesToast.show({ message: `Connection token copied for ${edge.name}`, type: 'success', duration: 3000 });
|
||||||
|
} else {
|
||||||
|
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
iconName: 'lucide:trash2',
|
iconName: 'lucide:trash2',
|
||||||
type: ['row'],
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
actionFunc: async (actionData: any) => {
|
||||||
|
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||||
await appstate.remoteIngressStatePart.dispatchAction(
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
appstate.deleteRemoteIngressAction,
|
appstate.deleteRemoteIngressAction,
|
||||||
edge.id,
|
edge.id,
|
||||||
@@ -277,8 +442,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
return status?.publicIp || '-';
|
return status?.publicIp || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPortsHtml(ports: number[]): TemplateResult {
|
private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
||||||
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
|
const manualPorts = edge.manualPorts || [];
|
||||||
|
const derivedPorts = edge.derivedPorts || [];
|
||||||
|
if (manualPorts.length === 0 && derivedPorts.length === 0) {
|
||||||
|
return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
|
||||||
|
}
|
||||||
|
return html`<div class="portsDisplay">${manualPorts.map(p => html`<span class="portBadge manual">${p}</span>`)}${derivedPorts.map(p => html`<span class="portBadge derived">${p}</span>`)}${derivedPorts.length > 0 ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEdgeTunnelCount(edgeId: string): number {
|
private getEdgeTunnelCount(edgeId: string): number {
|
||||||
|
|||||||
@@ -250,13 +250,20 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
const threatLevel = this.calculateThreatLevel(metrics);
|
const threatLevel = this.calculateThreatLevel(metrics);
|
||||||
const threatScore = this.getThreatScore(metrics);
|
const threatScore = this.getThreatScore(metrics);
|
||||||
|
|
||||||
|
// Derive active sessions from recent successful auth events (last hour)
|
||||||
|
const allEvents: any[] = metrics.recentEvents || [];
|
||||||
|
const oneHourAgo = Date.now() - 3600000;
|
||||||
|
const recentAuthSuccesses = allEvents.filter(
|
||||||
|
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
|
||||||
|
).length;
|
||||||
|
|
||||||
const tiles: IStatsTile[] = [
|
const tiles: IStatsTile[] = [
|
||||||
{
|
{
|
||||||
id: 'threatLevel',
|
id: 'threatLevel',
|
||||||
title: 'Threat Level',
|
title: 'Threat Level',
|
||||||
value: threatScore,
|
value: threatScore,
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
icon: 'shield',
|
icon: 'lucide:Shield',
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -271,27 +278,27 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
{
|
{
|
||||||
id: 'blockedThreats',
|
id: 'blockedThreats',
|
||||||
title: 'Blocked Threats',
|
title: 'Blocked Threats',
|
||||||
value: metrics.blockedIPs.length + metrics.spamDetected,
|
value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'userShield',
|
icon: 'lucide:ShieldCheck',
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
description: 'Total threats blocked today',
|
description: 'Total threats blocked today',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'activeSessions',
|
id: 'activeSessions',
|
||||||
title: 'Active Sessions',
|
title: 'Active Sessions',
|
||||||
value: 0,
|
value: recentAuthSuccesses,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'users',
|
icon: 'lucide:Users',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: 'Current authenticated sessions',
|
description: 'Authenticated in last hour',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'authFailures',
|
id: 'authFailures',
|
||||||
title: 'Auth Failures',
|
title: 'Auth Failures',
|
||||||
value: metrics.authenticationFailures,
|
value: metrics.authenticationFailures,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lockOpen',
|
icon: 'lucide:LockOpen',
|
||||||
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Failed login attempts today',
|
description: 'Failed login attempts today',
|
||||||
},
|
},
|
||||||
@@ -349,27 +356,41 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderAuthentication(metrics: any) {
|
private renderAuthentication(metrics: any) {
|
||||||
|
// Derive auth events from recentEvents
|
||||||
|
const allEvents: any[] = metrics.recentEvents || [];
|
||||||
|
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
|
||||||
|
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
|
||||||
|
|
||||||
const tiles: IStatsTile[] = [
|
const tiles: IStatsTile[] = [
|
||||||
{
|
{
|
||||||
id: 'authFailures',
|
id: 'authFailures',
|
||||||
title: 'Authentication Failures',
|
title: 'Authentication Failures',
|
||||||
value: metrics.authenticationFailures,
|
value: metrics.authenticationFailures,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lockOpen',
|
icon: 'lucide:LockOpen',
|
||||||
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Failed authentication attempts today',
|
description: 'Failed authentication attempts today',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'successfulLogins',
|
id: 'successfulLogins',
|
||||||
title: 'Successful Logins',
|
title: 'Successful Logins',
|
||||||
value: 0,
|
value: successfulLogins,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lock',
|
icon: 'lucide:Lock',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
description: 'Successful logins today',
|
description: 'Successful logins today',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Map auth events to login history table data
|
||||||
|
const loginHistory = authEvents.map((evt: any) => ({
|
||||||
|
timestamp: evt.timestamp,
|
||||||
|
username: evt.details?.username || 'unknown',
|
||||||
|
ipAddress: evt.ipAddress || 'unknown',
|
||||||
|
success: evt.success ?? false,
|
||||||
|
reason: evt.success ? '' : evt.message || 'Authentication failed',
|
||||||
|
}));
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-statsgrid
|
<dees-statsgrid
|
||||||
.tiles=${tiles}
|
.tiles=${tiles}
|
||||||
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Login History'}
|
.heading1=${'Login History'}
|
||||||
.heading2=${'Recent authentication attempts'}
|
.heading2=${'Recent authentication attempts'}
|
||||||
.data=${[]}
|
.data=${loginHistory}
|
||||||
.displayFunction=${(item) => ({
|
.displayFunction=${(item) => ({
|
||||||
'Time': new Date(item.timestamp).toLocaleString(),
|
'Time': new Date(item.timestamp).toLocaleString(),
|
||||||
'Username': item.username,
|
'Username': item.username,
|
||||||
@@ -399,7 +420,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Malware Detection',
|
title: 'Malware Detection',
|
||||||
value: metrics.malwareDetected,
|
value: metrics.malwareDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'virusSlash',
|
icon: 'lucide:BugOff',
|
||||||
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
|
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Malware detected',
|
description: 'Malware detected',
|
||||||
},
|
},
|
||||||
@@ -408,7 +429,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Phishing Detection',
|
title: 'Phishing Detection',
|
||||||
value: metrics.phishingDetected,
|
value: metrics.phishingDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'fishFins',
|
icon: 'lucide:Fish',
|
||||||
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
|
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Phishing attempts detected',
|
description: 'Phishing attempts detected',
|
||||||
},
|
},
|
||||||
@@ -417,7 +438,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Suspicious Activities',
|
title: 'Suspicious Activities',
|
||||||
value: metrics.suspiciousActivities,
|
value: metrics.suspiciousActivities,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'triangleExclamation',
|
icon: 'lucide:TriangleAlert',
|
||||||
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
|
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
|
||||||
description: 'Suspicious activities detected',
|
description: 'Suspicious activities detected',
|
||||||
},
|
},
|
||||||
@@ -426,7 +447,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
title: 'Spam Detection',
|
title: 'Spam Detection',
|
||||||
value: metrics.spamDetected,
|
value: metrics.spamDetected,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'ban',
|
icon: 'lucide:Ban',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
description: 'Spam emails blocked',
|
description: 'Spam emails blocked',
|
||||||
},
|
},
|
||||||
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
private getThreatScore(metrics: any): number {
|
private getThreatScore(metrics: any): number {
|
||||||
// Simple scoring algorithm
|
// Simple scoring algorithm
|
||||||
let score = 100;
|
let score = 100;
|
||||||
score -= metrics.blockedIPs.length * 2;
|
const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
|
||||||
score -= metrics.authenticationFailures * 1;
|
score -= blockedCount * 2;
|
||||||
score -= metrics.spamDetected * 0.5;
|
score -= (metrics.authenticationFailures || 0) * 1;
|
||||||
score -= metrics.malwareDetected * 3;
|
score -= (metrics.spamDetected || 0) * 0.5;
|
||||||
score -= metrics.phishingDetected * 3;
|
score -= (metrics.malwareDetected || 0) * 3;
|
||||||
score -= metrics.suspiciousActivities * 2;
|
score -= (metrics.phishingDetected || 0) * 3;
|
||||||
|
score -= (metrics.suspiciousActivities || 0) * 2;
|
||||||
return Math.max(0, Math.min(100, Math.round(score)));
|
return Math.max(0, Math.min(100, Math.round(score)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSecurityEvents(metrics: any): any[] {
|
private getSecurityEvents(metrics: any): any[] {
|
||||||
// Mock data - in real implementation, this would come from the server
|
const events: any[] = metrics.recentEvents || [];
|
||||||
return [
|
return events.map((evt: any) => ({
|
||||||
{
|
timestamp: evt.timestamp,
|
||||||
timestamp: Date.now() - 1000 * 60 * 5,
|
event: evt.message,
|
||||||
event: 'Multiple failed login attempts',
|
severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
|
||||||
severity: 'warning',
|
details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
|
||||||
details: 'IP: 192.168.1.100',
|
}));
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamp: Date.now() - 1000 * 60 * 15,
|
|
||||||
event: 'SPF check failed',
|
|
||||||
severity: 'medium',
|
|
||||||
details: 'Domain: example.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamp: Date.now() - 1000 * 60 * 30,
|
|
||||||
event: 'IP blocked due to spam',
|
|
||||||
severity: 'high',
|
|
||||||
details: 'IP: 10.0.0.1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearBlockedIPs() {
|
private async clearBlockedIPs() {
|
||||||
console.log('Clear blocked IPs');
|
// SmartProxy manages IP blocking — not yet exposed via API
|
||||||
|
alert('Clearing blocked IPs is not yet supported from the UI.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unblockIP(ip: string) {
|
private async unblockIP(ip: string) {
|
||||||
console.log('Unblock IP:', ip);
|
// SmartProxy manages IP blocking — not yet exposed via API
|
||||||
|
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveEmailSecuritySettings() {
|
private async saveEmailSecuritySettings() {
|
||||||
console.log('Save email security settings');
|
// Config is read-only from the UI for now
|
||||||
|
alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,15 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- Expiry date monitoring and alerts
|
- Expiry date monitoring and alerts
|
||||||
- Per-domain backoff status for failed provisions
|
- Per-domain backoff status for failed provisions
|
||||||
- One-click reprovisioning per domain
|
- One-click reprovisioning per domain
|
||||||
|
- Certificate import, export, and deletion
|
||||||
|
|
||||||
|
### 🌍 Remote Ingress Management
|
||||||
|
- Edge node registration with name, ports, and tags
|
||||||
|
- Real-time connection status (connected/disconnected/disabled)
|
||||||
|
- Public IP and active tunnel count per edge
|
||||||
|
- Auto-derived port display with manual/derived breakdown
|
||||||
|
- **Connection token generation** — one-click "Copy Token" for easy edge provisioning
|
||||||
|
- Enable/disable, edit, secret regeneration, and delete actions
|
||||||
|
|
||||||
### 📜 Log Viewer
|
### 📜 Log Viewer
|
||||||
- Real-time log streaming
|
- Real-time log streaming
|
||||||
@@ -85,6 +94,7 @@ ts_web/
|
|||||||
├── ops-view-network.ts # Network monitoring
|
├── ops-view-network.ts # Network monitoring
|
||||||
├── ops-view-emails.ts # Email queue management
|
├── ops-view-emails.ts # Email queue management
|
||||||
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||||
|
├── ops-view-remoteingress.ts # Remote ingress edge management
|
||||||
├── ops-view-logs.ts # Log viewer
|
├── ops-view-logs.ts # Log viewer
|
||||||
├── ops-view-config.ts # Configuration display
|
├── ops-view-config.ts # Configuration display
|
||||||
├── ops-view-security.ts # Security dashboard
|
├── ops-view-security.ts # Security dashboard
|
||||||
@@ -106,6 +116,8 @@ The app uses `@push.rocks/smartstate` with multiple state parts:
|
|||||||
| `logStatePart` | Soft | Recent logs, streaming status, filters |
|
| `logStatePart` | Soft | Recent logs, streaming status, filters |
|
||||||
| `networkStatePart` | Soft | Connections, IPs, throughput rates |
|
| `networkStatePart` | Soft | Connections, IPs, throughput rates |
|
||||||
| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list |
|
| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list |
|
||||||
|
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
|
||||||
|
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
|
||||||
|
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
@@ -128,6 +140,23 @@ fetchSecurityIncidentsAction() // Security events
|
|||||||
fetchBounceRecordsAction() // Bounce records
|
fetchBounceRecordsAction() // Bounce records
|
||||||
resendEmailAction(emailId) // Re-queue failed email
|
resendEmailAction(emailId) // Re-queue failed email
|
||||||
removeFromSuppressionAction(email) // Remove from suppression list
|
removeFromSuppressionAction(email) // Remove from suppression list
|
||||||
|
|
||||||
|
// Certificates
|
||||||
|
fetchCertificateOverviewAction() // All certificates with summary
|
||||||
|
reprovisionCertificateAction(domain) // Reprovision a certificate
|
||||||
|
deleteCertificateAction(domain) // Delete a certificate
|
||||||
|
importCertificateAction(cert) // Import a certificate
|
||||||
|
fetchCertificateExport(domain) // Export (standalone function)
|
||||||
|
|
||||||
|
// Remote Ingress
|
||||||
|
fetchRemoteIngressAction() // Edges + statuses
|
||||||
|
createRemoteIngressAction(data) // Create new edge
|
||||||
|
updateRemoteIngressAction(data) // Update edge settings
|
||||||
|
deleteRemoteIngressAction(id) // Remove edge
|
||||||
|
regenerateRemoteIngressSecretAction(id) // New secret
|
||||||
|
toggleRemoteIngressAction(id, enabled) // Enable/disable
|
||||||
|
clearNewEdgeSecretAction() // Dismiss secret banner
|
||||||
|
fetchConnectionToken(edgeId) // Get connection token (standalone function)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Client-Side Routing
|
### Client-Side Routing
|
||||||
@@ -141,6 +170,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
|
|||||||
/emails/failed → Failed emails
|
/emails/failed → Failed emails
|
||||||
/emails/security → Security incidents
|
/emails/security → Security incidents
|
||||||
/certificates → Certificate management
|
/certificates → Certificate management
|
||||||
|
/remoteingress → Remote ingress edge management
|
||||||
/logs → Log viewer
|
/logs → Log viewer
|
||||||
/configuration → System configuration
|
/configuration → System configuration
|
||||||
/security → Security dashboard
|
/security → Security dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user