Compare commits

...

28 Commits

Author SHA1 Message Date
e58e24a92d v9.3.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 23:50:40 +00:00
12070bc7b5 feat(remoteingress): add TLS certificate resolution and passthrough for RemoteIngress tunnel 2026-02-26 23:50:40 +00:00
37d62c51f3 v9.2.0
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 23:15:00 +00:00
ea9427d46b feat(remoteingress): expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI 2026-02-26 23:15:00 +00:00
bc77321752 v9.1.10
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 21:34:01 +00:00
65aa546c1c fix(deps): bump @push.rocks/smartproxy to ^25.8.5 2026-02-26 21:34:01 +00:00
54484518dc v9.1.9
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:53:45 +00:00
6fe1247d4d fix(deps(smartmta)): bump @push.rocks/smartmta to ^5.3.0 2026-02-26 17:53:45 +00:00
e59d80a3b3 v9.1.8
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:42:06 +00:00
6c4feba711 fix(deps): bump @serve.zone/remoteingress to ^4.1.0 2026-02-26 17:42:05 +00:00
006a9af20c v9.1.7
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:34:54 +00:00
dfb3b0ac37 fix(dcrouter): bump @push.rocks/smartproxy to ^25.8.4 and remove custom smartProxy timeout/connection lifetime settings from dcrouter 2026-02-26 17:34:54 +00:00
44c1a3a928 v9.1.6
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:14:52 +00:00
0c4e28455e fix(cleanup): prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior 2026-02-26 17:14:51 +00:00
cfc4cf378f v9.1.5
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 12:49:53 +00:00
a09e69a28b fix(remoteingress): Reconcile tunnel manager edge statuses with authoritative Rust hub periodically; update active tunnel counts and heartbeats, add missed edges, remove stale entries, and clear reconcile interval on stop 2026-02-26 12:49:53 +00:00
82dd19e274 v9.1.4
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-25 00:16:50 +00:00
c1d8afdbf7 fix(deps): bump @push.rocks/smartproxy to ^25.8.1 2026-02-25 00:16:50 +00:00
9b7426f1e6 v9.1.3
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 23:29:26 +00:00
3c9c865841 fix(deps): bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0 2026-02-24 23:29:26 +00:00
8421c9fe46 v9.1.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 20:58:43 +00:00
907e3df156 fix(deps): bump dependency versions for build and runtime packages 2026-02-24 20:58:43 +00:00
aaa0956148 v9.1.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:49:21 +00:00
118019fcf5 fix(dcrouter): no changes detected — no files modified, no release necessary 2026-02-23 21:49:21 +00:00
deb80f4fd0 v9.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:40:34 +00:00
7d28cea937 feat(ops-dashboard): add lucide icons to Ops dashboard view tabs 2026-02-23 21:40:34 +00:00
2bd5e5c7c5 v9.0.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:34:50 +00:00
4d6ac81c59 BREAKING CHANGE(opsserver): Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections 2026-02-23 21:34:50 +00:00
22 changed files with 1153 additions and 589 deletions

View File

@@ -0,0 +1,31 @@
[ 75ms] TypeError: Cannot read properties of null (reading 'appendChild')
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
[ 763ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 22315ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 22315ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 22316ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 22316ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 22321ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 22322ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 22322ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 22322ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 65371ms] [ERROR] method: >>createApiToken<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 65371ms] [ERROR] Failed to create token: zs @ http://localhost:3000/bundle.js:38142

View File

@@ -0,0 +1,25 @@
[ 642ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 114916ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 179731ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 179732ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 179737ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 179738ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 179738ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 179738ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13

View File

@@ -0,0 +1 @@
[ 603ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0

View File

@@ -0,0 +1,24 @@
[ 308ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 309ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 309ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 310ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 349ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 350ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 350ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 351ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
[ 500ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/apitokens:0

View File

@@ -0,0 +1,30 @@
[ 427ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
[ 44124ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
[ 59106ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 59106ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 59107ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 59107ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 59116ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 59116ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 89192ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 89192ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174

View File

@@ -1,5 +1,108 @@
# Changelog # Changelog
## 2026-02-26 - 9.3.0 - feat(remoteingress)
add TLS certificate resolution and passthrough for RemoteIngress tunnel
- Resolve TLS certs for the RemoteIngress tunnel with priority: explicit certPath/keyPath files → stored ACME cert for hubDomain → fallback to self-signed
- Expose tls option on ITunnelManagerConfig and forward certPem/keyPem into hub.start so the hub can use the provided TLS materials
- Add logging for cert selection and file read failures
- Bump dependency @serve.zone/remoteingress from ^4.2.0 to ^4.3.0
## 2026-02-26 - 9.2.0 - feat(remoteingress)
expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI
- Add detectedPublicIp to DC Router and populate it when a configured or auto-discovered public IP is chosen
- Use dcRouter.detectedPublicIp as a fallback for system.publicIp in the config handler
- Resolve proxy IPs from SmartProxy runtime settings when opts.proxyIps is not provided
- TunnelManager: capture peerAddr on edgeConnected and from Rust heartbeats, store per-edge publicIp, and add getConnectedEdgeIps()
- Expose connectedEdgeIps in the config API and return it in remoteIngress config
- Ops UI: show Connected Edge IPs, annotate 127.0.0.1 proxy IP as 'Remote Ingress' when applicable, and refresh remote ingress data during combined refresh when viewing remoteingress
- Bump dependency @serve.zone/remoteingress to ^4.2.0
## 2026-02-26 - 9.1.10 - fix(deps)
bump @push.rocks/smartproxy to ^25.8.5
- package.json: @push.rocks/smartproxy version updated from ^25.8.4 to ^25.8.5
- No other files changed
## 2026-02-26 - 9.1.9 - fix(deps(smartmta))
bump @push.rocks/smartmta to ^5.3.0
- Updated @push.rocks/smartmta from ^5.2.6 to ^5.3.0 in package.json
- Patch release recommended (no source code changes)
## 2026-02-26 - 9.1.8 - fix(deps)
bump @serve.zone/remoteingress to ^4.1.0
- Updated dependency @serve.zone/remoteingress from ^4.0.1 to ^4.1.0 in package.json
- Non-breaking dependency update; recommend patch version bump
## 2026-02-26 - 9.1.7 - fix(dcrouter)
bump @push.rocks/smartproxy to ^25.8.4 and remove custom smartProxy timeout/connection lifetime settings from dcrouter
- Bumped dependency @push.rocks/smartproxy from ^25.8.3 to ^25.8.4 in package.json
- Removed explicit smartProxy options: socketTimeout, inactivityTimeout, keepAliveInactivityMultiplier, extendedKeepAliveLifetime, and maxConnectionLifetime from ts/classes.dcrouter.ts
## 2026-02-26 - 9.1.6 - fix(cleanup)
prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior
- Tightened smartProxy connection timeouts and lifetimes (5m socketTimeout, 10m inactivityTimeout, keep-alive multiplier, 1h extendedKeepAliveLifetime, 4h maxConnectionLifetime).
- Remove event listeners before stopping services to avoid leaks (smartProxy, emailServer, dnsServer, remote ingress hub).
- OpsServer.stop now invokes logsHandler.cleanup to tear down active log streams and avoid duplicate push destinations.
- LogsHandler rewritten to use a module-level singleton push destination, track active stream stop callbacks, add cleanup(), guard against hung VirtualStream.sendData with a 10s timeout, and ensure intervals are cleared on stop.
- updateSmartProxyConfig removes listeners on the old instance before stopping it.
- Dependency bumps: @api.global/typedsocket ^4.1.2, @push.rocks/smartdata ^7.1.0, @push.rocks/smartmta ^5.2.6, @push.rocks/smartproxy ^25.8.3.
## 2026-02-26 - 9.1.5 - fix(remoteingress)
Reconcile tunnel manager edge statuses with authoritative Rust hub periodically; update active tunnel counts and heartbeats, add missed edges, remove stale entries, and clear reconcile interval on stop
- Add reconcile() to sync TS-side edgeStatuses with hub.getStatus and overwrite activeTunnels with the authoritative activeStreams.
- Start a periodic reconcile (setInterval every 15s) and store the interval handle on the tunnel manager.
- Clear the reconcile interval in stop() to avoid background timers; remove edgeStatuses entries that are no longer connected in Rust.
- Bump dependency @serve.zone/remoteingress from ^4.0.0 to ^4.0.1.
## 2026-02-25 - 9.1.4 - fix(deps)
bump @push.rocks/smartproxy to ^25.8.1
- Updated package.json dependency @push.rocks/smartproxy from ^25.8.0 to ^25.8.1
## 2026-02-24 - 9.1.3 - fix(deps)
bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0
- Updated @api.global/typedserver from ^8.3.1 to ^8.4.0
- Updated @push.rocks/smartproxy from ^25.7.9 to ^25.8.0
## 2026-02-24 - 9.1.2 - fix(deps)
bump dependency versions for build and runtime packages
- @git.zone/tsbundle: ^2.8.3 -> ^2.9.0
- @git.zone/tswatch: ^3.1.0 -> ^3.2.0
- @api.global/typedserver: ^8.3.0 -> ^8.3.1
- @design.estate/dees-catalog: ^3.43.2 -> ^3.43.3
## 2026-02-23 - 9.1.1 - fix(dcrouter)
no changes detected — no files modified, no release necessary
- Git diff contained no changes
- No files added, modified, or deleted
## 2026-02-23 - 9.1.0 - feat(ops-dashboard)
add lucide icons to Ops dashboard view tabs
- Added iconName property to 10 view tabs in ts_web/elements/ops-dashboard.ts to enable icons in the UI
- Icon mappings: Overview -> lucide:layoutDashboard, Configuration -> lucide:settings, Network -> lucide:network, Emails -> lucide:mail, Logs -> lucide:scrollText, Routes -> lucide:route, ApiTokens -> lucide:key, Security -> lucide:shield, Certificates -> lucide:badgeCheck, RemoteIngress -> lucide:globe
- Improves visual clarity of dashboard navigation
## 2026-02-23 - 9.0.0 - BREAKING CHANGE(opsserver)
Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
- Introduce IConfigData interface with typed sections: system, smartProxy, email, dns, tls, cache, radius, remoteIngress.
- Replace ConfigHandler.getConfiguration implementation to assemble and return IConfigData (changes API response shape for getConfiguration).
- Refactor frontend: update appstate types and ops-view-config to render the new config sections, use @serve.zone/catalog IConfigField/IConfigSectionAction, add uptime formatting and remote ingress UI.
- Fix ops-view-apitokens form handling to correctly read dees-input-tags values.
- Update tests to expect new configuration fields.
- Bump dependency @serve.zone/catalog to ^2.5.0.
## 2026-02-23 - 8.1.0 - feat(route-management) ## 2026-02-23 - 8.1.0 - feat(route-management)
add programmatic route management API with API tokens and admin UI add programmatic route management API with API tokens and admin UI

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "8.1.0", "version": "9.3.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": {
@@ -20,24 +20,24 @@
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.8.3", "@git.zone/tsbundle": "^2.9.0",
"@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.2.0",
"@types/node": "^25.3.0" "@types/node": "^25.3.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.2.6", "@api.global/typedrequest": "^3.2.6",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.3.0", "@api.global/typedserver": "^8.4.0",
"@api.global/typedsocket": "^4.1.0", "@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.2", "@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartacme": "^9.1.3",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdata": "^7.1.0",
"@push.rocks/smartdns": "^7.9.0", "@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
@@ -45,19 +45,19 @@
"@push.rocks/smartlog": "^3.2.1", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.1", "@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.3.0",
"@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.9", "@push.rocks/smartproxy": "^25.8.5",
"@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/catalog": "^2.3.0", "@serve.zone/catalog": "^2.5.0",
"@serve.zone/interfaces": "^5.3.0", "@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.0.0", "@serve.zone/remoteingress": "^4.3.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"

279
pnpm-lock.yaml generated
View File

@@ -15,17 +15,17 @@ importers:
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
'@api.global/typedserver': '@api.global/typedserver':
specifier: ^8.3.0 specifier: ^8.4.0
version: 8.3.0(@tiptap/pm@2.27.2) version: 8.4.0(@tiptap/pm@2.27.2)
'@api.global/typedsocket': '@api.global/typedsocket':
specifier: ^4.1.0 specifier: ^4.1.2
version: 4.1.0(@push.rocks/smartserve@2.0.1) version: 4.1.2(@push.rocks/smartserve@2.0.1)
'@apiclient.xyz/cloudflare': '@apiclient.xyz/cloudflare':
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.43.2 specifier: ^3.43.3
version: 3.43.2(@tiptap/pm@2.27.2) version: 3.43.3(@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
@@ -39,8 +39,8 @@ importers:
specifier: ^9.1.3 specifier: ^9.1.3
version: 9.1.3(socks@2.8.7) version: 9.1.3(socks@2.8.7)
'@push.rocks/smartdata': '@push.rocks/smartdata':
specifier: ^7.0.15 specifier: ^7.1.0
version: 7.0.15(socks@2.8.7) version: 7.1.0(socks@2.8.7)
'@push.rocks/smartdns': '@push.rocks/smartdns':
specifier: ^7.9.0 specifier: ^7.9.0
version: 7.9.0 version: 7.9.0
@@ -63,8 +63,8 @@ importers:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(socks@2.8.7) version: 5.1.0(socks@2.8.7)
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.2.2 specifier: ^5.3.0
version: 5.2.2 version: 5.3.0
'@push.rocks/smartnetwork': '@push.rocks/smartnetwork':
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^25.7.9 specifier: ^25.8.5
version: 25.7.9 version: 25.8.5
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -93,14 +93,14 @@ importers:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
'@serve.zone/catalog': '@serve.zone/catalog':
specifier: ^2.3.0 specifier: ^2.5.0
version: 2.3.0(@tiptap/pm@2.27.2) version: 2.5.0(@tiptap/pm@2.27.2)
'@serve.zone/interfaces': '@serve.zone/interfaces':
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
'@serve.zone/remoteingress': '@serve.zone/remoteingress':
specifier: ^4.0.0 specifier: ^4.3.0
version: 4.0.0 version: 4.3.0
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -115,8 +115,8 @@ importers:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
'@git.zone/tsbundle': '@git.zone/tsbundle':
specifier: ^2.8.3 specifier: ^2.9.0
version: 2.8.3 version: 2.9.0
'@git.zone/tsrun': '@git.zone/tsrun':
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
@@ -124,8 +124,8 @@ importers:
specifier: ^3.1.8 specifier: ^3.1.8
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3) version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
'@git.zone/tswatch': '@git.zone/tswatch':
specifier: ^3.1.0 specifier: ^3.2.0
version: 3.1.0(@tiptap/pm@2.27.2) version: 3.2.0(@tiptap/pm@2.27.2)
'@types/node': '@types/node':
specifier: ^25.3.0 specifier: ^25.3.0
version: 25.3.0 version: 25.3.0
@@ -144,8 +144,8 @@ packages:
'@api.global/typedserver@3.0.80': '@api.global/typedserver@3.0.80':
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==} resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
'@api.global/typedserver@8.3.0': '@api.global/typedserver@8.4.0':
resolution: {integrity: sha512-Uh2sQkoQXbsKFb/fhSm7P9oCCEnawGY7R5/9VgCLQUuFV30G0FL0oBTKZNqFli0CNNDDs0nQHE+dpdf4VHhlXQ==} resolution: {integrity: sha512-qOa5jUwiuHEoY1ZuLZiuU1unPl5JNd99Vv+hNAwgEIgAZd4TTy/mjdTp7lyviBzkGf2pROeCmAbDJJF8YQFCSA==}
'@api.global/typedsocket@3.1.1': '@api.global/typedsocket@3.1.1':
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==} resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
@@ -155,8 +155,8 @@ packages:
'@push.rocks/smartserve': '@push.rocks/smartserve':
optional: true optional: true
'@api.global/typedsocket@4.1.0': '@api.global/typedsocket@4.1.2':
resolution: {integrity: sha512-ttmoU5BNHmLAkAF/o+Ta8F5O4F7CUmkFo6LK7NKHQvuYJvodPMYWdhJ6yCINTF4pfCgljkMDUqoVKobm6ea4mQ==} resolution: {integrity: sha512-fZFuJY9ucFCICjF4wi6OvK8drsv6UcwVVsfamOT1HxFj7OBOYw6QHOceQ+cAQ8IrWbX817sf8gzlesl+jlG8JA==}
peerDependencies: peerDependencies:
'@push.rocks/smartserve': '>=1.1.0' '@push.rocks/smartserve': '>=1.1.0'
@@ -351,11 +351,14 @@ packages:
'@cloudflare/workers-types@4.20260210.0': '@cloudflare/workers-types@4.20260210.0':
resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==} resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==}
'@cloudflare/workers-types@4.20260303.0':
resolution: {integrity: sha512-soUlr4NJVkh5dR09RwtziTMbBQ+lbdoEesTGw8WUlvmnQ2M4h7CmJzAjC6a7IivUodiiCSjbLcGV/8PyZpvZkA==}
'@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.43.2': '@design.estate/dees-catalog@3.43.3':
resolution: {integrity: sha512-7pU+K+B70SxqR4DrBkpc/xvQGLDkxAV2jH7Qyh0TYgkCoxxjsxCuTMKM9JguA38wm6bEgBJVTvyg5S3wCwxm4Q==} resolution: {integrity: sha512-GjTePdwqNBL4isMOx4Ibei6pgK55H+DccbtgyNqjHRBz3LL14mo809ebjY2IZOVobswyzuTcNFvhfiqFP4/HLg==}
'@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==}
@@ -558,8 +561,8 @@ packages:
resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==} resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==}
hasBin: true hasBin: true
'@git.zone/tsbundle@2.8.3': '@git.zone/tsbundle@2.9.0':
resolution: {integrity: sha512-9q+KbVGKUTDNND+jDiJuk4bPH/mtiA2B0EWtV+/NyvgZfIbpe/ItHemyIvXB4RAqncMdBhzXquCFCvGjAhwVIQ==} resolution: {integrity: sha512-itXX/oiJjrRHUlIGTHUEqSwPuGwsG4Cq8kh7aqFOm8mYzJwtXYE1gBqLJTWZma6gI5n+xAk5qTxTyfikuPgWQA==}
hasBin: true hasBin: true
'@git.zone/tspublish@1.11.0': '@git.zone/tspublish@1.11.0':
@@ -574,8 +577,8 @@ packages:
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==} resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
hasBin: true hasBin: true
'@git.zone/tswatch@3.1.0': '@git.zone/tswatch@3.2.0':
resolution: {integrity: sha512-R2ZI+j1OKVgd0zTbtGtJjyt7r2kF0Z4nl8neolHuQL+jpr16i2NHVfVK92uIeeZDnJSqo5vf7Syt0XeQ4rz2HA==} resolution: {integrity: sha512-AlV2HPGPy1s91LwQ8x3a0UwHqe5/P2SRSXfHco5yo+D59KjkV7FaHLWwU+Dk03VNgmdkdUusPiB6/Cfkn1tPzw==}
hasBin: true hasBin: true
'@happy-dom/global-registrator@15.11.7': '@happy-dom/global-registrator@15.11.7':
@@ -684,74 +687,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.93': '@napi-rs/canvas-android-arm64@0.1.95':
resolution: {integrity: sha512-xRIoOPFvneR29Dtq5d9p2AJbijDCFeV4jQ+5Ms/xVAXJVb8R0Jlu+pPr/SkhrG+Mouaml4roPSXugTIeRl6CMA==} resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.93': '@napi-rs/canvas-darwin-arm64@0.1.95':
resolution: {integrity: sha512-daNDi76HN5grC6GXDmpxdfP+N2mQPd3sCfg62VyHwUuvbZh32P7R/IUjkzAxtYMtTza+Zvx9hfLJ3J7ENL6WMA==} resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.93': '@napi-rs/canvas-darwin-x64@0.1.95':
resolution: {integrity: sha512-1YfuNPIQLawsg/gSNdJRk4kQWUy9M/Gy8FGsOI79nhQEJ2PZdqpSPl5UNzf4elfuNXuVbEbmmjP68EQdUunDuQ==} resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
resolution: {integrity: sha512-8kEkOQPZjuyHjupvXExuJZiuiVNecdABGq3DLI7aO1EvQFOOlWMm2d/8Q5qXdV73Tn+nu3m16+kPajsN1oJefQ==} resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.93': '@napi-rs/canvas-linux-arm64-gnu@0.1.95':
resolution: {integrity: sha512-qIKLKkBkYSyWSYAoDThoxf5y1gr4X0g7W8rDU7d2HDeAAcotdVHUwuKkMeNe6+5VNk7/95EIhbslQjSxiCu32g==} resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.93': '@napi-rs/canvas-linux-arm64-musl@0.1.95':
resolution: {integrity: sha512-mAwQBGM3qArS9XEO21AK4E1uGvCuUCXjhIZk0dlVvs49MQ6wAAuCkYKNFpSKeSicKrLWwBMfgWX4qZoPh+M00A==} resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93': '@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
resolution: {integrity: sha512-kaIH5MpPzOZfkM+QMsBxGdM9jlJT+N+fwz2IEaju/S+DL65E5TgPOx4QcD5dQ8vsMxlak6uDrudBc4ns5xzZCw==} resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.93': '@napi-rs/canvas-linux-x64-gnu@0.1.95':
resolution: {integrity: sha512-KtMZJqYWvOSeW5w3VSV2f5iGnwNdKJm4gwgVid4xNy1NFi+NJSyuglA1lX1u4wIPxizyxh8OW5c5Usf6oSOMNQ==} resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.93': '@napi-rs/canvas-linux-x64-musl@0.1.95':
resolution: {integrity: sha512-qRZhOvlDBooRLX6V3/t9X9B+plZK+OrPLgfFixu0A1RO/3VHbubOknfnMnocSDAqk/L6cRyKI83VP2ciR9UO7w==} resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.93': '@napi-rs/canvas-win32-arm64-msvc@0.1.95':
resolution: {integrity: sha512-um5XE44vF8bjkQEsH2iRSUP9fDeQGYbn/qjM/v4whXG83qsqapAXlOPOQqSARZB1SiNvPUAuXoRsJLlKFmAEFw==} resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.93': '@napi-rs/canvas-win32-x64-msvc@0.1.95':
resolution: {integrity: sha512-maHlizZgmKsAPJwjwBZMnsWfq3Ca9QutoteQwKe7YqsmbECoylrLCCOGCDOredstW4BRWqRTfCl6NJaVVeAQvQ==} resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas@0.1.93': '@napi-rs/canvas@0.1.95':
resolution: {integrity: sha512-unVFo8CUlUeJCCxt50+j4yy91NF4x6n9zdGcvEsOFAWzowtZm3mgx8X2D7xjwV0cFSfxmpGPoe+JS77uzeFsxg==} resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -891,8 +894,8 @@ packages:
'@push.rocks/smartdata@5.16.7': '@push.rocks/smartdata@5.16.7':
resolution: {integrity: sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==} resolution: {integrity: sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==}
'@push.rocks/smartdata@7.0.15': '@push.rocks/smartdata@7.1.0':
resolution: {integrity: sha512-j09BUekmjiGZuvXmdGBiIpBTXFFnxrzG4rOBjZvPO/hG1BwNrvSkIVq20mIwdYomn8JGgya6oJ4Y7NL+FKTqEA==} resolution: {integrity: sha512-ots0g7/96R2xs4ww4F2/2rIwAOPT5AmzP3ciD31YsF02o5WA4Gg6C5laLBUjV3hXCjazhzFsRVQTfwbjmPQe4w==}
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
@@ -993,12 +996,11 @@ packages:
'@push.rocks/smartmongo@5.1.0': '@push.rocks/smartmongo@5.1.0':
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==} resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
'@push.rocks/smartmta@5.2.2': '@push.rocks/smartmta@5.3.0':
resolution: {integrity: sha512-0xKUi2BMM0HFYIPdNeNJZFitAiJ9CNbLlOJ8TenT+xInp7DKcSQ7ABER1rJKinPtvDjRDSiSqiF2iQR+O7299g==} resolution: {integrity: sha512-uJI25fslzvrcenU36WCdt5gB8cCfkjUlY7PqlxEtFp474/l/kZxNnvirv1gnZLRNNa+ioe5aH18HKE+KcAjuxA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
cpu: [x64, arm64] cpu: [x64, arm64]
os: [darwin, linux, win32] os: [darwin, linux, win32]
hasBin: true
'@push.rocks/smartmustache@3.0.2': '@push.rocks/smartmustache@3.0.2':
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==} resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
@@ -1033,8 +1035,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.9': '@push.rocks/smartproxy@25.8.5':
resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==} resolution: {integrity: sha512-oLmV+Bq7sSgQP9McTao/imb6Xb62QM7wlTFt5kNynrS5WK2wAe8cEjDKOcyu8N/WmzNCEClT5f/0xAtI6JxtkA==}
'@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==}
@@ -1057,6 +1059,9 @@ packages:
'@push.rocks/smartrust@1.2.1': '@push.rocks/smartrust@1.2.1':
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==} resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
'@push.rocks/smartrust@1.3.1':
resolution: {integrity: sha512-3ApbgF6yGeE2TRQxBY9Y48H1JlpcRheIp7QDBLSSfk80Uoe6fjdgBAfNz3Ir8hW3RZ3b7hA3sm1ZshCok58SEA==}
'@push.rocks/smartrx@3.0.10': '@push.rocks/smartrx@3.0.10':
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==} resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
@@ -1121,8 +1126,8 @@ packages:
'@push.rocks/taskbuffer@3.5.0': '@push.rocks/taskbuffer@3.5.0':
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==} resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
'@push.rocks/taskbuffer@4.2.0': '@push.rocks/taskbuffer@4.2.1':
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==} resolution: {integrity: sha512-F3aizWLGWdAz7wSJqOzjwVgo1VQJcxTbHUjDN/Pqxw0WMQUwODRGbhgy4zLag7bOyE4tc8Jv7yid7Bjmn5hKdg==}
'@push.rocks/taskbuffer@6.1.2': '@push.rocks/taskbuffer@6.1.2':
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==} resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
@@ -1333,14 +1338,14 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0': '@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.3.0': '@serve.zone/catalog@2.5.0':
resolution: {integrity: sha512-KCIQZXBO5A93VIsRkI/UzApNImEHzuA7P3Wx33+mDVUZ8/I5hafuCLgPzNu1q/TgQUte+q6I6e5Erezc9Hn74Q==} resolution: {integrity: sha512-bRwk7pbDxUB471wUAS7p22MTOOBCHlMWijsE43K9tDAPcxlRarhtf2Dgx0Y25s/dFXqj2JHwe6jjE84S80jFzg==}
'@serve.zone/interfaces@5.3.0': '@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@4.0.0': '@serve.zone/remoteingress@4.3.0':
resolution: {integrity: sha512-kcC9pnQdS5Mw0ypmwT3GIYwrjVQJOjWw5TJ+j46t6Y98S4Mb7wEAAPsU+YRXozpkqUxWUDBeDK1fQHLvoF9PmQ==} resolution: {integrity: sha512-yk14uS6oWIP83Zpem4hGf8zi3W9pefnxijtSWp45WvZ+u9XTXIADQNaUZBSTCId8CYkfPkfRGaaaARunVdjFXg==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -2006,6 +2011,10 @@ packages:
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
bare-events@2.8.2: bare-events@2.8.2:
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
peerDependencies: peerDependencies:
@@ -2072,6 +2081,10 @@ packages:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
brace-expansion@5.0.3:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==} resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
@@ -3231,8 +3244,12 @@ packages:
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@10.2.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.3:
resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==}
minimatch@9.0.5: minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
@@ -4165,7 +4182,7 @@ packages:
hasBin: true hasBin: true
wordwrap@1.0.0: wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
@@ -4334,13 +4351,13 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@api.global/typedserver@8.3.0(@tiptap/pm@2.27.2)': '@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.6
'@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.2(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260303.0
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.43.3(@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
@@ -4364,7 +4381,7 @@ snapshots:
'@push.rocks/smartserve': 2.0.1 '@push.rocks/smartserve': 2.0.1
'@push.rocks/smartsitemap': 2.0.4 '@push.rocks/smartsitemap': 2.0.4
'@push.rocks/smartstream': 3.2.5 '@push.rocks/smartstream': 3.2.5
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartwatch': 6.3.0 '@push.rocks/smartwatch': 6.3.0
'@push.rocks/taskbuffer': 3.5.0 '@push.rocks/taskbuffer': 3.5.0
'@push.rocks/webrequest': 4.0.2 '@push.rocks/webrequest': 4.0.2
@@ -4400,7 +4417,7 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@2.0.1)': '@api.global/typedsocket@4.1.2(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.6
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
@@ -4934,11 +4951,13 @@ snapshots:
'@cloudflare/workers-types@4.20260210.0': {} '@cloudflare/workers-types@4.20260210.0': {}
'@cloudflare/workers-types@4.20260303.0': {}
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.43.2(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.43.3(@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
@@ -5163,7 +5182,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@git.zone/tsbundle@2.8.3': '@git.zone/tsbundle@2.9.0':
dependencies: dependencies:
'@push.rocks/early': 4.0.4 '@push.rocks/early': 4.0.4
'@push.rocks/npmextra': 5.3.3 '@push.rocks/npmextra': 5.3.3
@@ -5220,7 +5239,7 @@ snapshots:
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)': '@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1) '@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
'@git.zone/tsbundle': 2.8.3 '@git.zone/tsbundle': 2.9.0
'@git.zone/tsrun': 2.0.1 '@git.zone/tsrun': 2.0.1
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
@@ -5266,10 +5285,10 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@git.zone/tswatch@3.1.0(@tiptap/pm@2.27.2)': '@git.zone/tswatch@3.2.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedserver': 8.3.0(@tiptap/pm@2.27.2) '@api.global/typedserver': 8.4.0(@tiptap/pm@2.27.2)
'@git.zone/tsbundle': 2.8.3 '@git.zone/tsbundle': 2.9.0
'@git.zone/tsrun': 2.0.1 '@git.zone/tsrun': 2.0.1
'@push.rocks/early': 4.0.4 '@push.rocks/early': 4.0.4
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
@@ -5282,7 +5301,7 @@ snapshots:
'@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
'@push.rocks/taskbuffer': 4.2.0 '@push.rocks/taskbuffer': 4.2.1
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- '@swc/helpers' - '@swc/helpers'
@@ -5447,52 +5466,52 @@ snapshots:
dependencies: dependencies:
sparse-bitfield: 3.0.3 sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.93': '@napi-rs/canvas-android-arm64@0.1.95':
optional: true optional: true
'@napi-rs/canvas-darwin-arm64@0.1.93': '@napi-rs/canvas-darwin-arm64@0.1.95':
optional: true optional: true
'@napi-rs/canvas-darwin-x64@0.1.93': '@napi-rs/canvas-darwin-x64@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.93': '@napi-rs/canvas-linux-arm64-gnu@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.93': '@napi-rs/canvas-linux-arm64-musl@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93': '@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.93': '@napi-rs/canvas-linux-x64-gnu@0.1.95':
optional: true optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.93': '@napi-rs/canvas-linux-x64-musl@0.1.95':
optional: true optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.93': '@napi-rs/canvas-win32-arm64-msvc@0.1.95':
optional: true optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.93': '@napi-rs/canvas-win32-x64-msvc@0.1.95':
optional: true optional: true
'@napi-rs/canvas@0.1.93': '@napi-rs/canvas@0.1.95':
optionalDependencies: optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.93 '@napi-rs/canvas-android-arm64': 0.1.95
'@napi-rs/canvas-darwin-arm64': 0.1.93 '@napi-rs/canvas-darwin-arm64': 0.1.95
'@napi-rs/canvas-darwin-x64': 0.1.93 '@napi-rs/canvas-darwin-x64': 0.1.95
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.93 '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
'@napi-rs/canvas-linux-arm64-gnu': 0.1.93 '@napi-rs/canvas-linux-arm64-gnu': 0.1.95
'@napi-rs/canvas-linux-arm64-musl': 0.1.93 '@napi-rs/canvas-linux-arm64-musl': 0.1.95
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.93 '@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
'@napi-rs/canvas-linux-x64-gnu': 0.1.93 '@napi-rs/canvas-linux-x64-gnu': 0.1.95
'@napi-rs/canvas-linux-x64-musl': 0.1.93 '@napi-rs/canvas-linux-x64-musl': 0.1.95
'@napi-rs/canvas-win32-arm64-msvc': 0.1.93 '@napi-rs/canvas-win32-arm64-msvc': 0.1.95
'@napi-rs/canvas-win32-x64-msvc': 0.1.93 '@napi-rs/canvas-win32-x64-msvc': 0.1.95
optional: true optional: true
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -5743,7 +5762,7 @@ snapshots:
'@apiclient.xyz/cloudflare': 7.1.0 '@apiclient.xyz/cloudflare': 7.1.0
'@peculiar/x509': 1.14.3 '@peculiar/x509': 1.14.3
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdata': 7.0.15(socks@2.8.7) '@push.rocks/smartdata': 7.1.0(socks@2.8.7)
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 7.9.0 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -5906,7 +5925,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartdata@7.0.15(socks@2.8.7)': '@push.rocks/smartdata@7.1.0(socks@2.8.7)':
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
@@ -5915,7 +5934,7 @@ snapshots:
'@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/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.5.0 '@push.rocks/taskbuffer': 3.5.0
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
@@ -6214,14 +6233,14 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartmta@5.2.2': '@push.rocks/smartmta@5.3.0':
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.2.1 '@push.rocks/smartlog': 3.2.1
'@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.3.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
lru-cache: 11.2.6 lru-cache: 11.2.6
mailparser: 3.9.3 mailparser: 3.9.3
@@ -6321,13 +6340,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.7.9': '@push.rocks/smartproxy@25.8.5':
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.3.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
minimatch: 10.2.1 minimatch: 10.2.2
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
@@ -6384,6 +6403,10 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust@1.3.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrx@3.0.10': '@push.rocks/smartrx@3.0.10':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -6566,7 +6589,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/taskbuffer@4.2.0': '@push.rocks/taskbuffer@4.2.1':
dependencies: dependencies:
'@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
@@ -6574,7 +6597,7 @@ snapshots:
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@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.2.3
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
@@ -6785,9 +6808,9 @@ snapshots:
domhandler: 5.0.3 domhandler: 5.0.3
selderee: 0.11.0 selderee: 0.11.0
'@serve.zone/catalog@2.3.0(@tiptap/pm@2.27.2)': '@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
'@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
'@design.estate/dees-wcctools': 3.8.0 '@design.estate/dees-wcctools': 3.8.0
@@ -6804,10 +6827,10 @@ 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@4.0.0': '@serve.zone/remoteingress@4.3.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.3.1
'@sindresorhus/is@5.6.0': {} '@sindresorhus/is@5.6.0': {}
@@ -7621,6 +7644,8 @@ snapshots:
dependencies: dependencies:
jackspeak: 4.2.3 jackspeak: 4.2.3
balanced-match@4.0.4: {}
bare-events@2.8.2: {} bare-events@2.8.2: {}
bare-fs@4.5.3: bare-fs@4.5.3:
@@ -7693,6 +7718,10 @@ snapshots:
dependencies: dependencies:
balanced-match: 4.0.2 balanced-match: 4.0.2
brace-expansion@5.0.3:
dependencies:
balanced-match: 4.0.4
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
@@ -8340,7 +8369,7 @@ snapshots:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
inflight: 1.0.6 inflight: 1.0.6
inherits: 2.0.4 inherits: 2.0.4
minimatch: 3.1.2 minimatch: 3.1.3
once: 1.4.0 once: 1.4.0
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
@@ -9137,7 +9166,11 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.2 brace-expansion: 5.0.2
minimatch@3.1.2: minimatch@10.2.2:
dependencies:
brace-expansion: 5.0.3
minimatch@3.1.3:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
@@ -9397,7 +9430,7 @@ snapshots:
pdfjs-dist@4.10.38: pdfjs-dist@4.10.38:
optionalDependencies: optionalDependencies:
'@napi-rs/canvas': 0.1.93 '@napi-rs/canvas': 0.1.95
peberminta@0.9.0: {} peberminta@0.9.0: {}

View File

@@ -55,10 +55,14 @@ tap.test('should respond to configuration request', async () => {
const response = await configRequest.fire({}); const response = await configRequest.fire({});
expect(response).toHaveProperty('config'); expect(response).toHaveProperty('config');
expect(response.config).toHaveProperty('system');
expect(response.config).toHaveProperty('smartProxy');
expect(response.config).toHaveProperty('email'); expect(response.config).toHaveProperty('email');
expect(response.config).toHaveProperty('dns'); expect(response.config).toHaveProperty('dns');
expect(response.config).toHaveProperty('proxy'); expect(response.config).toHaveProperty('tls');
expect(response.config).toHaveProperty('security'); expect(response.config).toHaveProperty('cache');
expect(response.config).toHaveProperty('radius');
expect(response.config).toHaveProperty('remoteIngress');
}); });
tap.test('should handle log retrieval request', async () => { tap.test('should handle log retrieval request', async () => {

View File

@@ -106,10 +106,14 @@ tap.test('should allow read-only config access', async () => {
const response = await configRequest.fire({}); const response = await configRequest.fire({});
expect(response).toHaveProperty('config'); expect(response).toHaveProperty('config');
expect(response.config).toHaveProperty('system');
expect(response.config).toHaveProperty('smartProxy');
expect(response.config).toHaveProperty('email'); expect(response.config).toHaveProperty('email');
expect(response.config).toHaveProperty('dns'); expect(response.config).toHaveProperty('dns');
expect(response.config).toHaveProperty('proxy'); expect(response.config).toHaveProperty('tls');
expect(response.config).toHaveProperty('security'); expect(response.config).toHaveProperty('cache');
expect(response.config).toHaveProperty('radius');
expect(response.config).toHaveProperty('remoteIngress');
console.log('Configuration read successfully'); console.log('Configuration read successfully');
}); });

View File

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

View File

@@ -217,6 +217,9 @@ export class DcRouter {
public routeConfigManager?: RouteConfigManager; public routeConfigManager?: RouteConfigManager;
public apiTokenManager?: ApiTokenManager; public apiTokenManager?: ApiTokenManager;
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
public detectedPublicIp: string | null = null;
// DNS query logging rate limiter state // DNS query logging rate limiter state
private dnsLogWindow: number[] = []; private dnsLogWindow: number[] = [];
private dnsBatchCount: number = 0; private dnsBatchCount: number = 0;
@@ -903,6 +906,20 @@ export class DcRouter {
await this.opsServer.stop(); await this.opsServer.stop();
try { try {
// Remove event listeners before stopping services to prevent leaks
if (this.smartProxy) {
this.smartProxy.removeAllListeners();
}
if (this.emailServer) {
if ((this.emailServer as any).deliverySystem) {
(this.emailServer as any).deliverySystem.removeAllListeners();
}
this.emailServer.removeAllListeners();
}
if (this.dnsServer) {
this.dnsServer.removeAllListeners();
}
// Stop all services in parallel for faster shutdown // Stop all services in parallel for faster shutdown
await Promise.all([ await Promise.all([
// Stop cache cleaner if running // Stop cache cleaner if running
@@ -976,6 +993,7 @@ export class DcRouter {
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> { public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
// Stop existing SmartProxy if running // Stop existing SmartProxy if running
if (this.smartProxy) { if (this.smartProxy) {
this.smartProxy.removeAllListeners();
await this.smartProxy.stop(); await this.smartProxy.stop();
this.smartProxy = undefined; this.smartProxy = undefined;
} }
@@ -1103,6 +1121,11 @@ export class DcRouter {
try { try {
// Stop the unified email server which contains all components // Stop the unified email server which contains all components
if (this.emailServer) { if (this.emailServer) {
// Remove listeners before stopping to prevent leaks on config update cycles
if ((this.emailServer as any).deliverySystem) {
(this.emailServer as any).deliverySystem.removeAllListeners();
}
this.emailServer.removeAllListeners();
await this.emailServer.stop(); await this.emailServer.stop();
logger.log('info', 'Unified email server stopped'); logger.log('info', 'Unified email server stopped');
this.emailServer = undefined; this.emailServer = undefined;
@@ -1554,6 +1577,7 @@ export class DcRouter {
} else if (this.options.publicIp) { } else if (this.options.publicIp) {
// Use explicitly configured public IP // Use explicitly configured public IP
publicIp = this.options.publicIp; publicIp = this.options.publicIp;
this.detectedPublicIp = publicIp;
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`); logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
} else { } else {
// Auto-discover public IP using smartnetwork // Auto-discover public IP using smartnetwork
@@ -1564,6 +1588,7 @@ export class DcRouter {
if (publicIps.v4) { if (publicIps.v4) {
publicIp = publicIps.v4; publicIp = publicIps.v4;
this.detectedPublicIp = publicIp;
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`); logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
} else { } else {
logger.log('warn', 'Could not auto-discover public IPv4 address'); logger.log('warn', 'Could not auto-discover public IPv4 address');
@@ -1689,10 +1714,42 @@ export class DcRouter {
const currentRoutes = this.options.smartProxyConfig?.routes || []; const currentRoutes = this.options.smartProxyConfig?.routes || [];
this.remoteIngressManager.setRoutes(currentRoutes as any[]); this.remoteIngressManager.setRoutes(currentRoutes as any[]);
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
const riCfg = this.options.remoteIngressConfig;
let tlsConfig: { certPem: string; keyPem: string } | undefined;
// Priority 1: Explicit cert/key file paths
if (riCfg.tls?.certPath && riCfg.tls?.keyPath) {
try {
const certPem = plugins.fs.readFileSync(riCfg.tls.certPath, 'utf8');
const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
tlsConfig = { certPem, keyPem };
logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
} catch (err) {
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${err.message}`);
}
}
// Priority 2: Existing cert from SmartProxy cert store for hubDomain
if (!tlsConfig && riCfg.hubDomain) {
try {
const stored = await this.storageManager.getJSON(`/proxy-certs/${riCfg.hubDomain}`);
if (stored?.publicKey && stored?.privateKey) {
tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${riCfg.hubDomain}`);
}
} catch { /* no stored cert, fall through */ }
}
if (!tlsConfig) {
logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
}
// Create and start the tunnel manager // Create and start the tunnel manager
this.tunnelManager = new TunnelManager(this.remoteIngressManager, { this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
tunnelPort: this.options.remoteIngressConfig.tunnelPort ?? 8443, tunnelPort: riCfg.tunnelPort ?? 8443,
targetHost: '127.0.0.1', targetHost: '127.0.0.1',
tls: tlsConfig,
}); });
await this.tunnelManager.start(); await this.tunnelManager.start();

View File

@@ -70,6 +70,10 @@ export class OpsServer {
} }
public async stop() { public async stop() {
// Clean up log handler streams and push destination before stopping the server
if (this.logsHandler) {
this.logsHandler.cleanup();
}
if (this.server) { if (this.server) {
await this.server.stop(); await this.server.stop();
} }

View File

@@ -1,4 +1,5 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import * as paths from '../../paths.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';
@@ -17,7 +18,7 @@ export class ConfigHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
'getConfiguration', 'getConfiguration',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
const config = await this.getConfiguration(dataArg.section); const config = await this.getConfiguration();
return { return {
config, config,
section: dataArg.section, section: dataArg.section,
@@ -27,82 +28,174 @@ export class ConfigHandler {
); );
} }
private async getConfiguration(section?: string): Promise<{ private async getConfiguration(): Promise<interfaces.requests.IConfigData> {
email: {
enabled: boolean;
ports: number[];
maxMessageSize: number;
rateLimits: {
perMinute: number;
perHour: number;
perDay: number;
};
domains?: string[];
};
dns: {
enabled: boolean;
port: number;
nameservers: string[];
caching: boolean;
ttl: number;
};
proxy: {
enabled: boolean;
httpPort: number;
httpsPort: number;
maxConnections: number;
};
security: {
blockList: string[];
rateLimit: boolean;
spamDetection: boolean;
tlsRequired: boolean;
};
}> {
const dcRouter = this.opsServerRef.dcRouterRef; const dcRouter = this.opsServerRef.dcRouterRef;
const opts = dcRouter.options;
const resolvedPaths = dcRouter.resolvedPaths;
// Get email domains if email server is configured // --- System ---
const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.storage?.readFunction
? 'custom'
: opts.storage?.fsPath
? 'filesystem'
: 'memory';
// Resolve proxy IPs: fall back to SmartProxy's runtime proxyIPs if not in opts
let proxyIps = opts.proxyIps || [];
if (proxyIps.length === 0 && dcRouter.smartProxy) {
const spSettings = (dcRouter.smartProxy as any).settings;
if (spSettings?.proxyIPs?.length > 0) {
proxyIps = spSettings.proxyIPs;
}
}
const system: interfaces.requests.IConfigData['system'] = {
baseDir: resolvedPaths.dcrouterHomeDir,
dataDir: resolvedPaths.dataDir,
publicIp: opts.publicIp || dcRouter.detectedPublicIp || null,
proxyIps,
uptime: Math.floor(process.uptime()),
storageBackend,
storagePath: opts.storage?.fsPath || null,
};
// --- SmartProxy ---
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
if (opts.smartProxyConfig?.acme) {
const acme = opts.smartProxyConfig.acme;
acmeInfo = {
enabled: acme.enabled !== false,
accountEmail: acme.accountEmail || '',
useProduction: acme.useProduction !== false,
autoRenew: acme.autoRenew !== false,
renewThresholdDays: acme.renewThresholdDays || 30,
};
}
let routeCount = 0;
if (dcRouter.routeConfigManager) {
try {
const merged = await dcRouter.routeConfigManager.getMergedRoutes();
routeCount = merged.routes.length;
} catch {
routeCount = opts.smartProxyConfig?.routes?.length || 0;
}
} else if (opts.smartProxyConfig?.routes) {
routeCount = opts.smartProxyConfig.routes.length;
}
const smartProxy: interfaces.requests.IConfigData['smartProxy'] = {
enabled: !!dcRouter.smartProxy,
routeCount,
acme: acmeInfo,
};
// --- Email ---
let emailDomains: string[] = []; let emailDomains: string[] = [];
if (dcRouter.emailServer && dcRouter.emailServer.domainRegistry) { if (dcRouter.emailServer && (dcRouter.emailServer as any).domainRegistry) {
emailDomains = dcRouter.emailServer.domainRegistry.getAllDomains(); emailDomains = (dcRouter.emailServer as any).domainRegistry.getAllDomains();
} else if (dcRouter.options.emailConfig?.domains) { } else if (opts.emailConfig?.domains) {
// Fallback: get domains from email config options emailDomains = opts.emailConfig.domains.map((d: any) =>
emailDomains = dcRouter.options.emailConfig.domains.map(d =>
typeof d === 'string' ? d : d.domain typeof d === 'string' ? d : d.domain
); );
} }
return { let portMapping: Record<string, number> | null = null;
email: { if (opts.emailPortConfig?.portMapping) {
portMapping = {};
for (const [ext, int] of Object.entries(opts.emailPortConfig.portMapping)) {
portMapping[String(ext)] = int as number;
}
}
const email: interfaces.requests.IConfigData['email'] = {
enabled: !!dcRouter.emailServer, enabled: !!dcRouter.emailServer,
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [], ports: opts.emailConfig?.ports || [],
maxMessageSize: 10 * 1024 * 1024, // 10MB default portMapping,
rateLimits: { hostname: opts.emailConfig?.hostname || null,
perMinute: 10,
perHour: 100,
perDay: 1000,
},
domains: emailDomains, domains: emailDomains,
}, emailRouteCount: opts.emailConfig?.routes?.length || 0,
dns: { receivedEmailsPath: opts.emailPortConfig?.receivedEmailsPath || null,
};
// --- DNS ---
const dnsRecords = (opts.dnsRecords || []).map(r => ({
name: r.name,
type: r.type,
value: r.value,
ttl: r.ttl,
}));
const dns: interfaces.requests.IConfigData['dns'] = {
enabled: !!dcRouter.dnsServer, enabled: !!dcRouter.dnsServer,
port: 53, port: 53,
nameservers: dcRouter.options.dnsNsDomains || [], nsDomains: opts.dnsNsDomains || [],
caching: true, scopes: opts.dnsScopes || [],
ttl: 300, recordCount: dnsRecords.length,
}, records: dnsRecords,
proxy: { dnsChallenge: !!opts.dnsChallenge?.cloudflareApiKey,
enabled: !!dcRouter.smartProxy, };
httpPort: 80,
httpsPort: 443, // --- TLS ---
maxConnections: 1000, let tlsSource: 'acme' | 'static' | 'none' = 'none';
}, if (opts.tls?.certPath && opts.tls?.keyPath) {
security: { tlsSource = 'static';
blockList: [], } else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
rateLimit: true, tlsSource = 'acme';
spamDetection: true, }
tlsRequired: false,
}, const tls: interfaces.requests.IConfigData['tls'] = {
contactEmail: opts.tls?.contactEmail || opts.smartProxyConfig?.acme?.accountEmail || null,
domain: opts.tls?.domain || null,
source: tlsSource,
certPath: opts.tls?.certPath || null,
keyPath: opts.tls?.keyPath || null,
};
// --- Cache ---
const cacheConfig = opts.cacheConfig;
const cache: interfaces.requests.IConfigData['cache'] = {
enabled: cacheConfig?.enabled !== false,
storagePath: cacheConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
dbName: cacheConfig?.dbName || 'dcrouter',
defaultTTLDays: cacheConfig?.defaultTTLDays || 30,
cleanupIntervalHours: cacheConfig?.cleanupIntervalHours || 1,
ttlConfig: cacheConfig?.ttlConfig ? { ...cacheConfig.ttlConfig } as Record<string, number> : {},
};
// --- RADIUS ---
const radiusCfg = opts.radiusConfig;
const radius: interfaces.requests.IConfigData['radius'] = {
enabled: !!dcRouter.radiusServer,
authPort: radiusCfg?.authPort || null,
acctPort: radiusCfg?.acctPort || null,
bindAddress: radiusCfg?.bindAddress || null,
clientCount: radiusCfg?.clients?.length || 0,
vlanDefaultVlan: radiusCfg?.vlanAssignment?.defaultVlan ?? null,
vlanAllowUnknownMacs: radiusCfg?.vlanAssignment?.allowUnknownMacs ?? null,
vlanMappingCount: radiusCfg?.vlanAssignment?.mappings?.length || 0,
};
// --- Remote Ingress ---
const riCfg = opts.remoteIngressConfig;
const connectedEdgeIps = dcRouter.tunnelManager?.getConnectedEdgeIps() || [];
const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
enabled: !!dcRouter.remoteIngressManager,
tunnelPort: riCfg?.tunnelPort || null,
hubDomain: riCfg?.hubDomain || null,
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath),
connectedEdgeIps,
};
return {
system,
smartProxy,
email,
dns,
tls,
cache,
radius,
remoteIngress,
}; };
} }
} }

View File

@@ -3,8 +3,15 @@ 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, baseLogger } from '../../logger.js'; import { logBuffer, baseLogger } from '../../logger.js';
// Module-level singleton: the log push destination is added once and reuses
// the current OpsServer reference so it survives OpsServer restarts without
// accumulating duplicate destinations.
let logPushDestinationInstalled = false;
let currentOpsServerRef: OpsServer | null = null;
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
private activeStreamStops: Set<() => void> = new Set();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent // Add this handler's router to the parent
@@ -13,6 +20,20 @@ export class LogsHandler {
this.setupLogPushDestination(); this.setupLogPushDestination();
} }
/**
* Clean up all active log streams and deactivate the push destination.
* Called when OpsServer stops.
*/
public cleanup(): void {
// Stop all active follow-mode log streams
for (const stop of this.activeStreamStops) {
stop();
}
this.activeStreamStops.clear();
// Deactivate the push destination (it stays registered but becomes a no-op)
currentOpsServerRef = null;
}
private registerHandlers(): void { private registerHandlers(): void {
// Get Recent Logs Handler // Get Recent Logs Handler
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
@@ -30,8 +51,8 @@ export class LogsHandler {
return { return {
logs, logs,
total: logs.length, // TODO: Implement proper total count total: logs.length,
hasMore: false, // TODO: Implement proper pagination hasMore: false,
}; };
} }
) )
@@ -56,10 +77,11 @@ export class LogsHandler {
// Start streaming // Start streaming
streamLogs.start(); streamLogs.start();
// VirtualStream handles cleanup automatically // Track the stop function so we can clean up on shutdown
this.activeStreamStops.add(streamLogs.stop);
return { return {
logStream: virtualStream as any, // Cast to IVirtualStream interface logStream: virtualStream as any,
}; };
} }
) )
@@ -169,14 +191,26 @@ export class LogsHandler {
/** /**
* Add a log destination to the base logger that pushes entries * Add a log destination to the base logger that pushes entries
* to all connected ops_dashboard TypedSocket clients. * to all connected ops_dashboard TypedSocket clients.
*
* Uses a module-level singleton so the destination is added only once,
* even across OpsServer restart cycles. The destination reads
* `currentOpsServerRef` dynamically so it always uses the active server.
*/ */
private setupLogPushDestination(): void { private setupLogPushDestination(): void {
const opsServerRef = this.opsServerRef; // Update the module-level reference so the existing destination uses the new server
currentOpsServerRef = this.opsServerRef;
if (logPushDestinationInstalled) {
return; // destination already registered — just updated the ref
}
logPushDestinationInstalled = true;
baseLogger.addLogDestination({ baseLogger.addLogDestination({
async handleLog(logPackage: any) { async handleLog(logPackage: any) {
// Access the TypedSocket server instance from OpsServer const opsServer = currentOpsServerRef;
const typedsocket = opsServerRef.server?.typedserver?.typedsocket; if (!opsServer) return;
const typedsocket = opsServer.server?.typedserver?.typedsocket;
if (!typedsocket) return; if (!typedsocket) return;
let connections: any[]; let connections: any[];
@@ -220,8 +254,18 @@ export class LogsHandler {
stop: () => void; stop: () => void;
} { } {
let intervalId: NodeJS.Timeout | null = null; let intervalId: NodeJS.Timeout | null = null;
let stopped = false;
let logIndex = 0; let logIndex = 0;
const stop = () => {
stopped = true;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
this.activeStreamStops.delete(stop);
};
const start = () => { const start = () => {
if (!follow) { if (!follow) {
// Send existing logs and close // Send existing logs and close
@@ -236,13 +280,19 @@ export class LogsHandler {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
virtualStream.sendData(encoder.encode(logData)); virtualStream.sendData(encoder.encode(logData));
}); });
// VirtualStream doesn't have end() method - it closes automatically
}); });
return; return;
} }
// For follow mode, simulate real-time log streaming // For follow mode, simulate real-time log streaming
intervalId = setInterval(async () => { intervalId = setInterval(async () => {
if (stopped) {
// Guard: clear interval if stop() was called between ticks
clearInterval(intervalId!);
intervalId = null;
return;
}
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'];
@@ -266,28 +316,19 @@ export class LogsHandler {
const logData = JSON.stringify(logEntry); const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
try { try {
await virtualStream.sendData(encoder.encode(logData)); // Use a timeout to detect hung streams (sendData can hang if the
// VirtualStream's keepAlive loop has ended)
await Promise.race([
virtualStream.sendData(encoder.encode(logData)),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('stream send timeout')), 10_000)
),
]);
} catch { } catch {
// Stream closed or errored — clean up to prevent interval leak // Stream closed, errored, or timed out — clean up
clearInterval(intervalId!); stop();
intervalId = null;
} }
}, 2000); // Send a log every 2 seconds }, 2000);
// TODO: Hook into actual logger events
// logger.on('log', (logEntry) => {
// if (matchesCriteria(logEntry, level, service)) {
// virtualStream.sendData(formatLogEntry(logEntry));
// }
// });
};
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
// TODO: Unhook from logger events
}; };
return { start, stop }; return { start, stop };

View File

@@ -5,6 +5,10 @@ import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
export interface ITunnelManagerConfig { export interface ITunnelManagerConfig {
tunnelPort?: number; tunnelPort?: number;
targetHost?: string; targetHost?: string;
tls?: {
certPem?: string;
keyPem?: string;
};
} }
/** /**
@@ -15,6 +19,7 @@ export class TunnelManager {
private manager: RemoteIngressManager; private manager: RemoteIngressManager;
private config: ITunnelManagerConfig; private config: ITunnelManagerConfig;
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map(); private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) { constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
this.manager = manager; this.manager = manager;
@@ -22,12 +27,11 @@ export class TunnelManager {
this.hub = new plugins.remoteingress.RemoteIngressHub(); this.hub = new plugins.remoteingress.RemoteIngressHub();
// Listen for edge connect/disconnect events // Listen for edge connect/disconnect events
this.hub.on('edgeConnected', (data: { edgeId: string }) => { this.hub.on('edgeConnected', (data: { edgeId: string; peerAddr: string }) => {
const existing = this.edgeStatuses.get(data.edgeId);
this.edgeStatuses.set(data.edgeId, { this.edgeStatuses.set(data.edgeId, {
edgeId: data.edgeId, edgeId: data.edgeId,
connected: true, connected: true,
publicIp: existing?.publicIp ?? null, publicIp: data.peerAddr || null,
activeTunnels: 0, activeTunnels: 0,
lastHeartbeat: Date.now(), lastHeartbeat: Date.now(),
connectedAt: Date.now(), connectedAt: Date.now(),
@@ -61,20 +65,73 @@ export class TunnelManager {
await this.hub.start({ await this.hub.start({
tunnelPort: this.config.tunnelPort ?? 8443, tunnelPort: this.config.tunnelPort ?? 8443,
targetHost: this.config.targetHost ?? '127.0.0.1', targetHost: this.config.targetHost ?? '127.0.0.1',
tls: this.config.tls,
}); });
// Send allowed edges to the hub // Send allowed edges to the hub
await this.syncAllowedEdges(); await this.syncAllowedEdges();
// Periodically reconcile with authoritative Rust hub status
this.reconcileInterval = setInterval(() => {
this.reconcile().catch(() => {});
}, 15_000);
} }
/** /**
* Stop the tunnel hub. * Stop the tunnel hub.
*/ */
public async stop(): Promise<void> { public async stop(): Promise<void> {
if (this.reconcileInterval) {
clearInterval(this.reconcileInterval);
this.reconcileInterval = null;
}
// Remove event listeners before stopping to prevent leaks
this.hub.removeAllListeners();
await this.hub.stop(); await this.hub.stop();
this.edgeStatuses.clear(); this.edgeStatuses.clear();
} }
/**
* Reconcile TS-side edge statuses with the authoritative Rust hub status.
* Overwrites event-derived activeTunnels with the real activeStreams count.
*/
private async reconcile(): Promise<void> {
const hubStatus = await this.hub.getStatus();
if (!hubStatus || !hubStatus.connectedEdges) return;
const rustEdgeIds = new Set<string>();
for (const rustEdge of hubStatus.connectedEdges) {
rustEdgeIds.add(rustEdge.edgeId);
const existing = this.edgeStatuses.get(rustEdge.edgeId);
if (existing) {
existing.activeTunnels = rustEdge.activeStreams;
existing.lastHeartbeat = Date.now();
// Update peer address if available from Rust hub
if (rustEdge.peerAddr) {
existing.publicIp = rustEdge.peerAddr;
}
} else {
// Missed edgeConnected event — add entry
this.edgeStatuses.set(rustEdge.edgeId, {
edgeId: rustEdge.edgeId,
connected: true,
publicIp: rustEdge.peerAddr || null,
activeTunnels: rustEdge.activeStreams,
lastHeartbeat: Date.now(),
connectedAt: rustEdge.connectedAt * 1000,
});
}
}
// Remove entries for edges no longer connected in Rust (missed edgeDisconnected)
for (const edgeId of this.edgeStatuses.keys()) {
if (!rustEdgeIds.has(edgeId)) {
this.edgeStatuses.delete(edgeId);
}
}
}
/** /**
* Sync allowed edges from the manager to the hub. * Sync allowed edges from the manager to the hub.
* Call this after creating/deleting/updating edges. * Call this after creating/deleting/updating edges.
@@ -109,6 +166,19 @@ export class TunnelManager {
return count; return count;
} }
/**
* Get the public IPs of all connected edges.
*/
public getConnectedEdgeIps(): string[] {
const ips: string[] = [];
for (const status of this.edgeStatuses.values()) {
if (status.connected && status.publicIp) {
ips.push(status.publicIp);
}
}
return ips;
}
/** /**
* Get the total number of active tunnels across all edges. * Get the total number of active tunnels across all edges.
*/ */

View File

@@ -1,6 +1,79 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as authInterfaces from '../data/auth.js'; import * as authInterfaces from '../data/auth.js';
export interface IConfigData {
system: {
baseDir: string;
dataDir: string;
publicIp: string | null;
proxyIps: string[];
uptime: number;
storageBackend: 'filesystem' | 'custom' | 'memory';
storagePath: string | null;
};
smartProxy: {
enabled: boolean;
routeCount: number;
acme: {
enabled: boolean;
accountEmail: string;
useProduction: boolean;
autoRenew: boolean;
renewThresholdDays: number;
} | null;
};
email: {
enabled: boolean;
ports: number[];
portMapping: Record<string, number> | null;
hostname: string | null;
domains: string[];
emailRouteCount: number;
receivedEmailsPath: string | null;
};
dns: {
enabled: boolean;
port: number;
nsDomains: string[];
scopes: string[];
recordCount: number;
records: Array<{ name: string; type: string; value: string; ttl?: number }>;
dnsChallenge: boolean;
};
tls: {
contactEmail: string | null;
domain: string | null;
source: 'acme' | 'static' | 'none';
certPath: string | null;
keyPath: string | null;
};
cache: {
enabled: boolean;
storagePath: string | null;
dbName: string | null;
defaultTTLDays: number;
cleanupIntervalHours: number;
ttlConfig: Record<string, number>;
};
radius: {
enabled: boolean;
authPort: number | null;
acctPort: number | null;
bindAddress: string | null;
clientCount: number;
vlanDefaultVlan: number | null;
vlanAllowUnknownMacs: boolean | null;
vlanMappingCount: number;
};
remoteIngress: {
enabled: boolean;
tunnelPort: number | null;
hubDomain: string | null;
tlsConfigured: boolean;
connectedEdgeIps: string[];
};
}
// Get Configuration (read-only) // Get Configuration (read-only)
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
@@ -12,7 +85,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
section?: string; section?: string;
}; };
response: { response: {
config: any; config: IConfigData;
section?: string; section?: string;
}; };
} }

View File

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

View File

@@ -21,7 +21,7 @@ export interface IStatsState {
} }
export interface IConfigState { export interface IConfigState {
config: any | null; config: interfaces.requests.IConfigData | null;
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
} }
@@ -1321,6 +1321,15 @@ async function dispatchCombinedRefreshAction() {
console.error('Certificate refresh failed:', error); console.error('Certificate refresh failed:', error);
} }
} }
// Refresh remote ingress data if on remoteingress view
if (currentView === 'remoteingress') {
try {
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
} catch (error) {
console.error('Remote ingress refresh failed:', error);
}
}
} catch (error) { } catch (error) {
console.error('Combined refresh failed:', error); console.error('Combined refresh failed:', error);
} }

View File

@@ -43,42 +43,52 @@ export class OpsDashboard extends DeesElement {
private viewTabs = [ private viewTabs = [
{ {
name: 'Overview', name: 'Overview',
iconName: 'lucide:layoutDashboard',
element: OpsViewOverview, element: OpsViewOverview,
}, },
{
name: 'Configuration',
iconName: 'lucide:settings',
element: OpsViewConfig,
},
{ {
name: 'Network', name: 'Network',
iconName: 'lucide:network',
element: OpsViewNetwork, element: OpsViewNetwork,
}, },
{ {
name: 'Emails', name: 'Emails',
iconName: 'lucide:mail',
element: OpsViewEmails, element: OpsViewEmails,
}, },
{ {
name: 'Logs', name: 'Logs',
iconName: 'lucide:scrollText',
element: OpsViewLogs, element: OpsViewLogs,
}, },
{ {
name: 'Routes', name: 'Routes',
iconName: 'lucide:route',
element: OpsViewRoutes, element: OpsViewRoutes,
}, },
{ {
name: 'ApiTokens', name: 'ApiTokens',
iconName: 'lucide:key',
element: OpsViewApiTokens, element: OpsViewApiTokens,
}, },
{
name: 'Configuration',
element: OpsViewConfig,
},
{ {
name: 'Security', name: 'Security',
iconName: 'lucide:shield',
element: OpsViewSecurity, element: OpsViewSecurity,
}, },
{ {
name: 'Certificates', name: 'Certificates',
iconName: 'lucide:badgeCheck',
element: OpsViewCertificates, element: OpsViewCertificates,
}, },
{ {
name: 'RemoteIngress', name: 'RemoteIngress',
iconName: 'lucide:globe',
element: OpsViewRemoteIngress, element: OpsViewRemoteIngress,
}, },
]; ];

View File

@@ -225,13 +225,17 @@ export class OpsViewApiTokens extends DeesElement {
name: 'Create', name: 'Create',
iconName: 'lucide:key', iconName: 'lucide:key',
action: async (modalArg: any) => { action: async (modalArg: any) => {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); const contentEl = modalArg.shadowRoot?.querySelector('.content');
const form = contentEl?.querySelector('dees-form');
if (!form) return; if (!form) return;
const formData = await form.collectFormData(); const formData = await form.collectFormData();
if (!formData.name) return; if (!formData.name) return;
// dees-input-tags returns string[] directly // dees-input-tags is not in dees-form's FORM_INPUT_TYPES, so collectFormData() won't
const scopes = (formData.scopes || []) // include it. Query the tags input directly and call getValue().
const tagsInput = form.querySelector('dees-input-tags') as any;
const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
const scopes = rawScopes
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[]; .filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
const expiresInDays = formData.expiresInDays const expiresInDays = formData.expiresInDays

View File

@@ -1,6 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as shared from './shared/index.js'; import * as shared from './shared/index.js';
import * as appstate from '../appstate.js'; import * as appstate from '../appstate.js';
import { appRouter } from '../router.js';
import { import {
DeesElement, DeesElement,
@@ -12,6 +13,8 @@ import {
type TemplateResult, type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
@customElement('ops-view-config') @customElement('ops-view-config')
export class OpsViewConfig extends DeesElement { export class OpsViewConfig extends DeesElement {
@state() @state()
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
shared.viewHostCss, shared.viewHostCss,
css` css`
.configSection {
background: ${cssManager.bdTheme('#fff', '#222')};
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.sectionHeader {
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
padding: 16px 24px;
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
display: flex;
justify-content: space-between;
align-items: center;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#ccc')};
display: flex;
align-items: center;
gap: 12px;
}
.sectionTitle dees-icon {
font-size: 20px;
color: ${cssManager.bdTheme('#666', '#888')};
}
.sectionContent {
padding: 24px;
}
.configField {
margin-bottom: 20px;
}
.configField:last-child {
margin-bottom: 0;
}
.fieldLabel {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#666', '#999')};
margin-bottom: 8px;
display: block;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.fieldValue {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
color: ${cssManager.bdTheme('#333', '#ccc')};
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
padding: 10px 14px;
border-radius: 6px;
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
}
.fieldValue.empty {
color: ${cssManager.bdTheme('#999', '#666')};
font-style: italic;
}
.nestedFields {
margin-left: 16px;
padding-left: 16px;
border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
}
/* Status badge styles */
.statusBadge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
}
.statusBadge.enabled {
background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')};
color: ${cssManager.bdTheme('#155724', '#66cc66')};
}
.statusBadge.disabled {
background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')};
color: ${cssManager.bdTheme('#721c24', '#cc6666')};
}
.statusBadge dees-icon {
font-size: 14px;
}
/* Array/list display */
.arrayItems {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.arrayItem {
display: inline-flex;
align-items: center;
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
padding: 4px 12px;
border-radius: 16px;
font-size: 13px;
font-family: 'Consolas', 'Monaco', monospace;
}
.arrayCount {
font-size: 12px;
color: ${cssManager.bdTheme('#999', '#666')};
margin-bottom: 8px;
}
/* Numeric value formatting */
.numericValue {
font-weight: 600;
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
}
.errorMessage {
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
border-radius: 4px;
padding: 16px;
color: ${cssManager.bdTheme('#c00', '#ff6666')};
margin: 16px 0;
}
.loadingMessage { .loadingMessage {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
color: ${cssManager.bdTheme('#666', '#999')}; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
} }
.infoNote { .errorMessage {
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')}; background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')}; border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
margin-bottom: 24px; color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
color: ${cssManager.bdTheme('#004085', '#88ccff')}; margin: 16px 0;
display: flex;
align-items: center;
gap: 12px;
}
.infoNote dees-icon {
font-size: 20px;
flex-shrink: 0;
} }
`, `,
]; ];
@@ -202,185 +59,276 @@ export class OpsViewConfig extends DeesElement {
return html` return html`
<ops-sectionheading>Configuration</ops-sectionheading> <ops-sectionheading>Configuration</ops-sectionheading>
${this.configState.isLoading ? html` ${this.configState.isLoading
? html`
<div class="loadingMessage"> <div class="loadingMessage">
<dees-spinner></dees-spinner> <dees-spinner></dees-spinner>
<p>Loading configuration...</p> <p>Loading configuration...</p>
</div> </div>
` : this.configState.error ? html` `
: this.configState.error
? html`
<div class="errorMessage"> <div class="errorMessage">
Error loading configuration: ${this.configState.error} Error loading configuration: ${this.configState.error}
</div> </div>
` : this.configState.config ? html` `
<div class="infoNote"> : this.configState.config
<dees-icon icon="lucide:info"></dees-icon> ? this.renderConfig()
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span> : html`<div class="errorMessage">No configuration loaded</div>`}
</div>
${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)}
${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)}
${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)}
${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)}
` : html`
<div class="errorMessage">No configuration loaded</div>
`}
`; `;
} }
private renderConfigSection(key: string, title: string, icon: string, config: any) { private renderConfig(): TemplateResult {
const isEnabled = config?.enabled ?? false; const cfg = this.configState.config!;
return html` return html`
<div class="configSection"> <sz-config-overview
<div class="sectionHeader"> infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
<h3 class="sectionTitle"> @navigate=${(e: CustomEvent) => {
<dees-icon icon="${icon}"></dees-icon> if (e.detail?.view) {
${title} appRouter.navigateToView(e.detail.view);
</h3> }
${this.renderStatusBadge(isEnabled)} }}
</div> >
<div class="sectionContent"> ${this.renderSystemSection(cfg.system)}
${config ? this.renderConfigFields(config) : html` ${this.renderSmartProxySection(cfg.smartProxy)}
<div class="fieldValue empty">Not configured</div> ${this.renderEmailSection(cfg.email)}
`} ${this.renderDnsSection(cfg.dns)}
</div> ${this.renderTlsSection(cfg.tls)}
</div> ${this.renderCacheSection(cfg.cache)}
${this.renderRadiusSection(cfg.radius)}
${this.renderRemoteIngressSection(cfg.remoteIngress)}
</sz-config-overview>
`; `;
} }
private renderStatusBadge(enabled: boolean): TemplateResult { private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
return enabled // Annotate proxy IPs with source hint when Remote Ingress is active
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>` const ri = this.configState.config?.remoteIngress;
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`; let proxyIpValues: string[] | null = sys.proxyIps.length > 0 ? [...sys.proxyIps] : null;
if (proxyIpValues && ri?.enabled && proxyIpValues.includes('127.0.0.1')) {
proxyIpValues = proxyIpValues.map(ip =>
ip === '127.0.0.1' ? '127.0.0.1 (Remote Ingress)' : ip
);
} }
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] { const fields: IConfigField[] = [
if (!config || typeof config !== 'object') { { key: 'Base Directory', value: sys.baseDir },
return html`<div class="fieldValue">${this.formatValue(config)}</div>`; { key: 'Data Directory', value: sys.dataDir },
} { key: 'Public IP', value: sys.publicIp },
{ key: 'Proxy IPs', value: proxyIpValues, type: 'pills' },
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
{ key: 'Storage Path', value: sys.storagePath },
];
return Object.entries(config).map(([key, value]) => {
const fieldName = prefix ? `${prefix}.${key}` : key;
const displayName = this.formatFieldName(key);
// Handle boolean values with badges
if (typeof value === 'boolean') {
return html` return html`
<div class="configField"> <sz-config-section
<label class="fieldLabel">${displayName}</label> title="System"
${this.renderStatusBadge(value)} subtitle="Base paths and infrastructure"
</div> icon="lucide:server"
status="enabled"
.fields=${fields}
></sz-config-section>
`; `;
} }
// Handle arrays private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
if (Array.isArray(value)) { const fields: IConfigField[] = [
{ key: 'Route Count', value: proxy.routeCount },
];
if (proxy.acme) {
fields.push(
{ key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
{ key: 'Account Email', value: proxy.acme.accountEmail || null },
{ key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
{ key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
{ key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
);
}
const actions: IConfigSectionAction[] = [
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
];
return html` return html`
<div class="configField"> <sz-config-section
<label class="fieldLabel">${displayName}</label> title="SmartProxy"
${this.renderArrayValue(value, key)} subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
</div> icon="lucide:network"
.status=${proxy.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`; `;
} }
// Handle nested objects private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
if (typeof value === 'object' && value !== null) { const fields: IConfigField[] = [
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
{ key: 'Hostname', value: email.hostname },
{ key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
{ key: 'Email Routes', value: email.emailRouteCount },
{ key: 'Received Emails Path', value: email.receivedEmailsPath },
];
if (email.portMapping) {
const mappingStr = Object.entries(email.portMapping)
.map(([ext, int]) => `${ext}${int}`)
.join(', ');
fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
}
const actions: IConfigSectionAction[] = [
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
];
return html` return html`
<div class="configField"> <sz-config-section
<label class="fieldLabel">${displayName}</label> title="Email Server"
<div class="nestedFields"> subtitle="SMTP email handling with smartmta"
${this.renderConfigFields(value, fieldName)} icon="lucide:mail"
</div> .status=${email.enabled ? 'enabled' : 'disabled'}
</div> .fields=${fields}
.actions=${actions}
></sz-config-section>
`; `;
} }
// Handle primitive values private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Port', value: dns.port },
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
{ key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
{ key: 'Record Count', value: dns.recordCount },
{ key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
];
return html` return html`
<div class="configField"> <sz-config-section
<label class="fieldLabel">${displayName}</label> title="DNS Server"
<div class="fieldValue">${this.formatValue(value, key)}</div> subtitle="Authoritative DNS with smartdns"
</div> icon="lucide:globe"
.status=${dns.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
></sz-config-section>
`; `;
});
} }
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult { private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
if (arr.length === 0) { const fields: IConfigField[] = [
return html`<div class="fieldValue empty">None configured</div>`; { key: 'Contact Email', value: tls.contactEmail },
} { key: 'Domain', value: tls.domain },
{ key: 'Source', value: tls.source, type: 'badge' },
{ key: 'Certificate Path', value: tls.certPath },
{ key: 'Key Path', value: tls.keyPath },
];
// Determine if we should show as pills/tags const status = tls.source === 'none' ? 'not-configured' : 'enabled';
const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number'); const actions: IConfigSectionAction[] = [
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
];
if (showAsPills) {
const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
return html` return html`
<div class="arrayCount">${arr.length} ${itemLabel}</div> <sz-config-section
<div class="arrayItems"> title="TLS / Certificates"
${arr.map(item => html`<span class="arrayItem">${item}</span>`)} subtitle="Certificate management and ACME"
</div> icon="lucide:shield-check"
.status=${status as any}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`; `;
} }
// For complex arrays, show as JSON private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Storage Path', value: cache.storagePath },
{ key: 'DB Name', value: cache.dbName },
{ key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
{ key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
];
if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
for (const [key, val] of Object.entries(cache.ttlConfig)) {
fields.push({ key: `TTL: ${key}`, value: `${val} days` });
}
}
return html` return html`
<div class="fieldValue"> <sz-config-section
${arr.length} items configured title="Cache Database"
</div> subtitle="Persistent caching with smartdata"
icon="lucide:database"
.status=${cache.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
></sz-config-section>
`; `;
} }
private getArrayItemLabel(fieldKey: string, count: number): string { private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
const labels: Record<string, [string, string]> = { const fields: IConfigField[] = [
ports: ['port', 'ports'], { key: 'Auth Port', value: radius.authPort },
domains: ['domain', 'domains'], { key: 'Accounting Port', value: radius.acctPort },
nameservers: ['nameserver', 'nameservers'], { key: 'Bind Address', value: radius.bindAddress },
blockList: ['IP', 'IPs'], { key: 'Client Count', value: radius.clientCount },
}; ];
const label = labels[fieldKey] || ['item', 'items']; if (radius.vlanDefaultVlan !== null) {
return count === 1 ? label[0] : label[1]; fields.push(
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
);
} }
private formatFieldName(key: string): string { const status = radius.enabled ? 'enabled' : 'not-configured';
// Convert camelCase to readable format
return key return html`
.replace(/([A-Z])/g, ' $1') <sz-config-section
.replace(/^./, str => str.toUpperCase()) title="RADIUS Server"
.trim(); subtitle="Network authentication and VLAN assignment"
icon="lucide:wifi"
.status=${status as any}
.fields=${fields}
></sz-config-section>
`;
} }
private formatValue(value: any, fieldKey?: string): string | TemplateResult { private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
if (value === null || value === undefined) { const fields: IConfigField[] = [
return html`<span class="empty">Not set</span>`; { key: 'Tunnel Port', value: ri.tunnelPort },
{ key: 'Hub Domain', value: ri.hubDomain },
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
{ key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
];
const actions: IConfigSectionAction[] = [
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
];
return html`
<sz-config-section
title="Remote Ingress"
subtitle="Edge tunnel nodes"
icon="lucide:cloud"
.status=${ri.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`;
} }
if (typeof value === 'number') { private formatUptime(seconds: number): string {
// Format bytes const days = Math.floor(seconds / 86400);
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) { const hours = Math.floor((seconds % 86400) / 3600);
return html`<span class="numericValue">${this.formatBytes(value)}</span>`; const mins = Math.floor((seconds % 3600) / 60);
}
// Format time values
if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
return html`<span class="numericValue">${value} seconds</span>`;
}
// Format port numbers
if (fieldKey?.toLowerCase().includes('port')) {
return html`<span class="numericValue">${value}</span>`;
}
// Format counts with separators
return html`<span class="numericValue">${value.toLocaleString()}</span>`;
}
return String(value); const parts: string[] = [];
} if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
private formatBytes(bytes: number): string { parts.push(`${mins}m`);
if (bytes === 0) return '0 B'; return parts.join(' ');
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
} }
} }