Compare commits

...

32 Commits

Author SHA1 Message Date
71f23302d3 v10.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-03-03 11:49:28 +00:00
ecbaab3000 fix(deps): bump dependencies: @push.rocks/smartmetrics to ^3.0.2, @push.rocks/smartproxy to ^25.9.0, @serve.zone/remoteingress to ^4.4.0 2026-03-03 11:49:28 +00:00
8cb1f3c12d v10.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-03-03 07:29:03 +00:00
c7d7f92759 fix(ops-view-apitokens): use correct lucide icon name for roll/rotate actions in API tokens view 2026-03-03 07:29:03 +00:00
02e1b9231f v10.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-03-02 22:32:21 +00:00
4ec4dd2bdb fix(ts_web): use actionContext for dispatches in web state actions and bump @push.rocks/smartstate to ^2.2.0 2026-03-02 22:32:21 +00:00
aa543160e2 v10.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-03-02 15:06:26 +00:00
94fa0f04d8 fix(monitoring): use a per-second ring buffer for DNS query metrics, improve DNS logging rate limiting and security event aggregation, and bump smartmta dependency 2026-03-02 15:06:26 +00:00
17deb481e0 v10.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-03-02 12:37:44 +00:00
e452ffd38e fix(no-changes): no changes detected; no version bump required 2026-03-02 12:37:44 +00:00
865b4a53e6 v10.1.3
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-02 09:43:08 +00:00
c07f3975e9 fix(deps): bump @api.global/typedrequest to ^3.2.7 2026-03-02 09:43:08 +00:00
476505537a v10.1.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-01 00:44:01 +00:00
74ad5cec90 fix(core): improve shutdown cleanup, socket/stream robustness, and memory/cache handling 2026-03-01 00:44:01 +00:00
59a3f7978e v10.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-27 10:29:20 +00:00
7dc976b59e fix(ops-view-apitokens): replace lucide:refresh-cw with lucide:rotate-cw for Roll action icon 2026-02-27 10:29:20 +00:00
345effee13 v10.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-27 10:24:20 +00:00
dee6897931 feat(api-tokens): add ability to roll (regenerate) API token secrets and UI to display the newly generated token once 2026-02-27 10:24:20 +00:00
56f41d70b3 v10.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-27 00:04:24 +00:00
8f570ae8a0 BREAKING CHANGE(remote-ingress): replace tlsConfigured boolean with tlsMode (custom | acme | self-signed) and compute TLS mode server-side 2026-02-27 00:04:24 +00:00
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
27 changed files with 705 additions and 216 deletions

View File

@@ -0,0 +1,6 @@
[ 95ms] 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)
[ 992ms] [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,5 @@
[ 329ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 727ms] [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/routes:0
[ 260513ms] [ERROR] method: >>adminLoginWithUsernameAndPassword<< got an ERROR: "login failed" with data undefined @ http://localhost:3000/bundle.js:13
[ 260514ms] [ERROR] Login failed: Ns @ http://localhost:3000/bundle.js:38066
[ 260518ms] [WARNING] FontAwesome icon not found: circle-xmark @ http://localhost:3000/bundle.js:1203

View File

@@ -0,0 +1,3 @@
[ 397ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 657ms] [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/routes:0
[ 24180ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,129 @@
# Changelog # Changelog
## 2026-03-03 - 10.1.8 - fix(deps)
bump dependencies: @push.rocks/smartmetrics to ^3.0.2, @push.rocks/smartproxy to ^25.9.0, @serve.zone/remoteingress to ^4.4.0
- @push.rocks/smartmetrics: 3.0.1 -> 3.0.2 (patch)
- @push.rocks/smartproxy: 25.8.5 -> 25.9.0 (minor)
- @serve.zone/remoteingress: 4.3.0 -> 4.4.0 (minor)
## 2026-03-03 - 10.1.7 - fix(ops-view-apitokens)
use correct lucide icon name for roll/rotate actions in API tokens view
- Updated iconName from 'lucide:rotate-cw' to 'lucide:rotateCw' in ts_web/elements/ops-view-apitokens.ts (two occurrences) to match lucide icon naming and ensure icons render correctly
- Non-functional UI fix; no API or behavior changes
## 2026-03-02 - 10.1.6 - fix(ts_web)
use actionContext for dispatches in web state actions and bump @push.rocks/smartstate to ^2.2.0
- Action handlers in ts_web/appstate.ts now accept an actionContext parameter and call await actionContext.dispatch(...) instead of using statePartArg.dispatchAction(...).
- Handlers return the awaited dispatch result (ensuring callers receive refreshed state) instead of returning the previous statePartArg.getState().
- Dependency bumped in package.json: @push.rocks/smartstate from ^2.1.1 to ^2.2.0.
- Playwright artifacts (logs and page screenshots) were added under .playwright-mcp.
## 2026-03-02 - 10.1.5 - fix(monitoring)
use a per-second ring buffer for DNS query metrics, improve DNS logging rate limiting and security event aggregation, and bump smartmta dependency
- Replace unbounded query timestamp array with a fixed-size per-second Int32Array ring buffer (300s) to calculate queries-per-second with O(1) updates and bounded memory
- Add incrementQueryRing and getQueryRingSum helpers to correctly zero stale slots and sum recent seconds
- Change metrics cache interval from 200ms to 1000ms to better match dashboard polling and reduce update frequency
- Refactor DNS adaptive logging to use per-second counters (dnsLogWindowSecond / dnsLogWindowCount) instead of timestamp arrays to avoid per-query array filtering and improve rate limiting accuracy; reset counters on flush
- Security logger: avoid mutating source when sorting/filtering, and implement single-pass aggregation with optional time-window filtering for byLevel/byType/top lists
- Bump dependency @push.rocks/smartmta from ^5.3.0 to ^5.3.1
## 2026-03-02 - 10.1.4 - fix(no-changes)
no changes detected; no version bump required
- package version is 10.1.3
- git diff contains no changes
## 2026-03-02 - 10.1.3 - fix(deps)
bump @api.global/typedrequest to ^3.2.7
- Updated @api.global/typedrequest from ^3.2.6 to ^3.2.7 in package.json
- Dependency patch bump only — no source code changes detected
- Current package version 10.1.2 -> recommended next version 10.1.3 (patch)
## 2026-03-01 - 10.1.2 - fix(core)
improve shutdown cleanup, socket/stream robustness, and memory/cache handling
- Reset security singletons and CacheDb on shutdown to allow GC (SecurityLogger, ContentScanner, IPReputationChecker, CacheDb).
- Add DNS socket 'error' handler and only destroy socket when not already destroyed to avoid uncaught exceptions.
- Move pruning of dnsMetrics.queryTimestamps to a periodic interval to avoid O(n) work on every query.
- Debounce IPReputationChecker cache saves (save timer + reset on instance reset) to reduce IO and prevent duplicate saves.
- Fix virtualStream send timeout handling by keeping/clearing a timeout handle to avoid leaks and hung promises.
- Add memory store eviction in StorageManager to cap entries (MAX_MEMORY_ENTRIES) and evict oldest entries when exceeded.
- Add terminal-ready timeout in ops-view-logs to avoid blocking UI initialization if xterm CDN fails to initialize.
- Bump dev dependency @types/node and push.rocks/smartstate versions.
## 2026-02-27 - 10.1.1 - fix(ops-view-apitokens)
replace lucide:refresh-cw with lucide:rotate-cw for Roll action icon
- Updated ts_web/elements/ops-view-apitokens.ts: changed iconName in two locations to 'lucide:rotate-cw' for the Roll/Roll Token actions.
- UI-only change — no functional or API behavior modified.
- Current package version is 10.1.0; recommended patch bump to 10.1.1.
## 2026-02-27 - 10.1.0 - feat(api-tokens)
add ability to roll (regenerate) API token secrets and UI to display the newly generated token once
- Server: added ApiTokenManager.rollToken(id) to regenerate a token secret, update its hash, persist it and log the action.
- Server: added opsserver handler 'rollApiToken' which requires admin identity and returns the new raw token value (shown once) or error messages.
- API: added typed request interface IReq_RollApiToken for the rollApiToken RPC.
- Web: added appstate.rollApiToken wrapper to call the new typed request.
- UI: ops-view-apitokens updated with a 'Roll' action and a modal flow to confirm rolling, call the API, refresh token list, and present the new token value to copy (token value is shown only once).
- Security: operation is admin-only and the raw token is returned only once after rolling.
## 2026-02-27 - 10.0.0 - BREAKING CHANGE(remote-ingress)
replace tlsConfigured boolean with tlsMode ('custom' | 'acme' | 'self-signed') and compute TLS mode server-side
- Server: compute remoteIngress.tlsMode = 'custom' when custom certPath/keyPath provided; else attempt to detect ACME by checking stored certs for hubDomain; default to 'self-signed' as fallback.
- API: replaced remoteIngress.tlsConfigured:boolean with tlsMode:'custom'|'acme'|'self-signed' — this is a breaking change for consumers of the config API.
- UI: ops view updated to display TLS Mode as a badge instead of a boolean "TLS Configured" field.
- Action required: update clients and integrations to read remoteIngress.tlsMode instead of tlsConfigured.
## 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) ## 2026-02-26 - 9.1.6 - fix(cleanup)
prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "9.1.6", "version": "10.1.8",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -24,10 +24,10 @@
"@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.2.0", "@git.zone/tswatch": "^3.2.0",
"@types/node": "^25.3.0" "@types/node": "^25.3.3"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.2.6", "@api.global/typedrequest": "^3.2.7",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.4.0", "@api.global/typedserver": "^8.4.0",
"@api.global/typedsocket": "^4.1.2", "@api.global/typedsocket": "^4.1.2",
@@ -43,21 +43,21 @@
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.1", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.1", "@push.rocks/smartmetrics": "^3.0.2",
"@push.rocks/smartmongo": "^5.1.0", "@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.2.6", "@push.rocks/smartmta": "^5.3.1",
"@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.8.3", "@push.rocks/smartproxy": "^25.9.0",
"@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.2.0",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@serve.zone/catalog": "^2.5.0", "@serve.zone/catalog": "^2.5.0",
"@serve.zone/interfaces": "^5.3.0", "@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.0.1", "@serve.zone/remoteingress": "^4.4.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"

155
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@api.global/typedrequest': '@api.global/typedrequest':
specifier: ^3.2.6 specifier: ^3.2.7
version: 3.2.6 version: 3.2.7
'@api.global/typedrequest-interfaces': '@api.global/typedrequest-interfaces':
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
@@ -57,14 +57,14 @@ importers:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.2.1 version: 3.2.1
'@push.rocks/smartmetrics': '@push.rocks/smartmetrics':
specifier: ^3.0.1 specifier: ^3.0.2
version: 3.0.1 version: 3.0.2
'@push.rocks/smartmongo': '@push.rocks/smartmongo':
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(socks@2.8.7) version: 5.1.0(socks@2.8.7)
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.2.6 specifier: ^5.3.1
version: 5.2.6 version: 5.3.1
'@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.8.3 specifier: ^25.9.0
version: 25.8.3 version: 25.9.0
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -87,8 +87,8 @@ importers:
specifier: ^3.0.10 specifier: ^3.0.10
version: 3.0.10 version: 3.0.10
'@push.rocks/smartstate': '@push.rocks/smartstate':
specifier: ^2.0.30 specifier: ^2.2.0
version: 2.0.30 version: 2.2.0
'@push.rocks/smartunique': '@push.rocks/smartunique':
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
@@ -99,8 +99,8 @@ importers:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
'@serve.zone/remoteingress': '@serve.zone/remoteingress':
specifier: ^4.0.1 specifier: ^4.4.0
version: 4.0.1 version: 4.4.0
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -127,8 +127,8 @@ importers:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.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.3
version: 25.3.0 version: 25.3.3
packages: packages:
@@ -138,8 +138,8 @@ packages:
'@api.global/typedrequest-interfaces@3.0.19': '@api.global/typedrequest-interfaces@3.0.19':
resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==} resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==}
'@api.global/typedrequest@3.2.6': '@api.global/typedrequest@3.2.7':
resolution: {integrity: sha512-CnvbjYjnGGw3rwL+7bTHSgRHEpDujzhs3cv7l1xgCXMPQe3DcPg74+9ep1Y5cu21T/w0pxNnDCJpbb0SHqHzAw==} resolution: {integrity: sha512-9CC8EojPDraKlwWK3ZjM8/wJ9jguY/kc+pCgcd61epHFXTIKC8jYts3vKPmEkBPno5Ejn3JZgqp/GRzplCC51w==}
'@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==}
@@ -845,6 +845,9 @@ packages:
'@push.rocks/lik@6.2.2': '@push.rocks/lik@6.2.2':
resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==} resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==}
'@push.rocks/lik@6.3.1':
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
'@push.rocks/mongodump@1.1.0': '@push.rocks/mongodump@1.1.0':
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==} resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
@@ -981,8 +984,8 @@ packages:
'@push.rocks/smartmatch@2.0.0': '@push.rocks/smartmatch@2.0.0':
resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==} resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==}
'@push.rocks/smartmetrics@3.0.1': '@push.rocks/smartmetrics@3.0.2':
resolution: {integrity: sha512-wdc9v8F0dQXwraAVCZjGvt2lnGpbVYHWzER9OFR+u+ultMq7aLjUevxSpkS8BkhBMLTuNMCCWIqyAzaCqHzFgQ==} resolution: {integrity: sha512-bW60TrdCubsZwsYK1+lE9y+OoXN8MfB7bhSASlTKtwhExdCbjXYXnSt9W7zerD7HbsUNOsvejIjX9q4oju+P2g==}
'@push.rocks/smartmime@1.0.6': '@push.rocks/smartmime@1.0.6':
resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==} resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==}
@@ -996,8 +999,8 @@ packages:
'@push.rocks/smartmongo@5.1.0': '@push.rocks/smartmongo@5.1.0':
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==} resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
'@push.rocks/smartmta@5.2.6': '@push.rocks/smartmta@5.3.1':
resolution: {integrity: sha512-MJKgcsgcPicCezm6DCFkni2zdY+mMsfMaqeEjPorhadRCd0Qeo0jP6Ozz82+SjhKHrVHuPPCPJuDG37PsEUqsw==} resolution: {integrity: sha512-cEuXO56i/zL9eZS79eAesEW16ikdBJKLlEv9pLKkt2cmaHBWADGHjeOzJmsszQ9CSFcuhd41aHYVGMZXVvsG2g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
cpu: [x64, arm64] cpu: [x64, arm64]
os: [darwin, linux, win32] os: [darwin, linux, win32]
@@ -1035,8 +1038,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.8.3': '@push.rocks/smartproxy@25.9.0':
resolution: {integrity: sha512-ocFvNpB9UDxOt5R7mNcxI3vmVQDziANOXvbo0ApVLzVjWhHizUu2dvdfAgHW+rt8mACiBzrSiVNjwBA2pZhv7w==} resolution: {integrity: sha512-fMw47zDFCj8umeoi+85H/fFzdsKB6iZxhgqfYGiCoB/yL6j0jodHujxqyRavJtL1XFHjMIw95nwSVgz/mx0q8g==}
'@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==}
@@ -1083,8 +1086,8 @@ packages:
'@push.rocks/smartspawn@3.0.3': '@push.rocks/smartspawn@3.0.3':
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
'@push.rocks/smartstate@2.0.30': '@push.rocks/smartstate@2.2.0':
resolution: {integrity: sha512-IuNW8XtSumXIr7g7MIFyWg5PBwLF2mwsymTJbSEycK2Pa9ZLk4yjRHnR907xCilxgiMU9ixQZyNdpa5MMF999A==} resolution: {integrity: sha512-e41vA1y9b0HBauzjMSh3l0YlRhcG4jhArm43/HHNdT+inxEGIeRL24VGeq+sl2MUr/eFWqgrETXhvL3YrsYFaw==}
'@push.rocks/smartstream@2.0.8': '@push.rocks/smartstream@2.0.8':
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
@@ -1344,8 +1347,8 @@ packages:
'@serve.zone/interfaces@5.3.0': '@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@4.0.1': '@serve.zone/remoteingress@4.4.0':
resolution: {integrity: sha512-vl3nSGETsIR/BE1T2lvVGD1s4AMqh1CBAP7SNUnshXzYFzyFD2Fs1VmOEXP5V7grglawIuewhu+Th7eomC6zIA==} resolution: {integrity: sha512-+M2EHP0irezN0xutYn0H6ZXePWoMJy4ETbK9/5Zgb4nx2FbDRYQyFLJUXyLzVL/rtJ/5trToPwOa3ljZoVze3g==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -1835,11 +1838,11 @@ packages:
'@types/node@18.19.130': '@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@22.19.11': '@types/node@22.19.13':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==}
'@types/node@25.3.0': '@types/node@25.3.3':
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -4291,11 +4294,11 @@ snapshots:
'@api.global/typedrequest-interfaces@3.0.19': {} '@api.global/typedrequest-interfaces@3.0.19': {}
'@api.global/typedrequest@3.2.6': '@api.global/typedrequest@3.2.7':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.3.1
'@push.rocks/smartbuffer': 3.0.5 '@push.rocks/smartbuffer': 3.0.5
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartguard': 3.1.0 '@push.rocks/smartguard': 3.1.0
@@ -4305,7 +4308,7 @@ snapshots:
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)': '@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260210.0
@@ -4353,7 +4356,7 @@ snapshots:
'@api.global/typedserver@8.4.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.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260303.0 '@cloudflare/workers-types': 4.20260303.0
@@ -4399,7 +4402,7 @@ snapshots:
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)': '@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
@@ -4419,7 +4422,7 @@ snapshots:
'@api.global/typedsocket@4.1.2(@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.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4994,14 +4997,14 @@ snapshots:
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
broadcast-channel: 7.3.0 broadcast-channel: 7.3.0
'@design.estate/dees-domtools@2.3.8': '@design.estate/dees-domtools@2.3.8':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@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
@@ -5010,7 +5013,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrouter': 1.3.3 '@push.rocks/smartrouter': 1.3.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstate': 2.0.30 '@push.rocks/smartstate': 2.2.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 3.0.37
@@ -5334,7 +5337,7 @@ snapshots:
'@inquirer/figures': 1.0.15 '@inquirer/figures': 1.0.15
'@inquirer/type': 2.0.0 '@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4 '@types/mute-stream': 0.0.4
'@types/node': 22.19.11 '@types/node': 22.19.13
'@types/wrap-ansi': 3.0.0 '@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
cli-width: 4.1.0 cli-width: 4.1.0
@@ -5676,7 +5679,7 @@ snapshots:
'@push.rocks/levelcache@3.2.0': '@push.rocks/levelcache@3.2.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.3.1
'@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartcache': 1.0.18 '@push.rocks/smartcache': 1.0.18
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
@@ -5707,6 +5710,17 @@ snapshots:
'@types/symbol-tree': 3.2.5 '@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4 symbol-tree: 3.2.4
'@push.rocks/lik@6.3.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3
'@types/minimatch': 5.1.2
'@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4
'@push.rocks/mongodump@1.1.0(socks@2.8.7)': '@push.rocks/mongodump@1.1.0(socks@2.8.7)':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
@@ -5751,7 +5765,7 @@ snapshots:
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@configvault.io/interfaces': 1.0.17 '@configvault.io/interfaces': 1.0.17
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -6165,7 +6179,7 @@ snapshots:
dependencies: dependencies:
matcher: 5.0.0 matcher: 5.0.0
'@push.rocks/smartmetrics@3.0.1': '@push.rocks/smartmetrics@3.0.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -6233,7 +6247,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartmta@5.2.6': '@push.rocks/smartmta@5.3.1':
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
@@ -6340,7 +6354,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.8.3': '@push.rocks/smartproxy@25.9.0':
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
@@ -6424,7 +6438,7 @@ snapshots:
'@push.rocks/smartserve@2.0.1': '@push.rocks/smartserve@2.0.1':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@cfworker/json-schema': 4.1.1 '@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
@@ -6487,9 +6501,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartstate@2.0.30': '@push.rocks/smartstate@2.2.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartjson': 6.0.0 '@push.rocks/smartjson': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -6827,7 +6840,7 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@serve.zone/remoteingress@4.0.1': '@serve.zone/remoteingress@4.4.0':
dependencies: dependencies:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartrust': 1.3.1 '@push.rocks/smartrust': 1.3.1
@@ -7359,22 +7372,22 @@ snapshots:
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -7382,7 +7395,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 1.2.1 '@types/send': 1.2.1
@@ -7395,17 +7408,17 @@ snapshots:
'@types/from2@2.3.6': '@types/from2@2.3.6':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7427,12 +7440,12 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -7455,26 +7468,26 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
form-data: 4.0.5 form-data: 4.0.5
'@types/node-forge@1.3.14': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/node@18.19.130': '@types/node@18.19.130':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
'@types/node@22.19.11': '@types/node@22.19.13':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.3.0': '@types/node@25.3.3':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.18.2
@@ -7492,22 +7505,22 @@ snapshots:
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7537,11 +7550,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -8018,7 +8031,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.19 '@types/cors': 2.8.19
'@types/node': 25.3.0 '@types/node': 25.3.3
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2

View File

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

View File

@@ -23,6 +23,7 @@ import { MetricsManager } from './monitoring/index.js';
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js'; import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js'; import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { RouteConfigManager, ApiTokenManager } from './config/index.js'; import { RouteConfigManager, ApiTokenManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
export interface IDcRouterOptions { export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -217,8 +218,12 @@ 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 dnsLogWindowSecond: number = 0; // epoch second of current window
private dnsLogWindowCount: number = 0; // queries logged this second
private dnsBatchCount: number = 0; private dnsBatchCount: number = 0;
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null; private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
@@ -476,12 +481,6 @@ export class DcRouter {
...this.options.smartProxyConfig, ...this.options.smartProxyConfig,
routes, routes,
acme: acmeConfig, acme: acmeConfig,
// Tighter connection timeouts to prevent connection buildup
socketTimeout: 300_000, // 5 min idle socket timeout
inactivityTimeout: 600_000, // 10 min inactivity timeout
keepAliveInactivityMultiplier: 3, // keep-alive idle = 30 min (10min * 3)
extendedKeepAliveLifetime: 3_600_000, // keep-alive connections live max 1 hour
maxConnectionLifetime: 14_400_000, // absolute max connection lifetime 4 hours
certStore: { certStore: {
loadAll: async () => { loadAll: async () => {
const keys = await this.storageManager.list('/proxy-certs/'); const keys = await this.storageManager.list('/proxy-certs/');
@@ -903,7 +902,8 @@ export class DcRouter {
} }
this.dnsBatchTimer = null; this.dnsBatchTimer = null;
this.dnsBatchCount = 0; this.dnsBatchCount = 0;
this.dnsLogWindow = []; this.dnsLogWindowSecond = 0;
this.dnsLogWindowCount = 0;
} }
await this.opsServer.stop(); await this.opsServer.stop();
@@ -959,6 +959,7 @@ export class DcRouter {
// Stop cache database after other services (they may need it during shutdown) // Stop cache database after other services (they may need it during shutdown)
if (this.cacheDb) { if (this.cacheDb) {
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) })); await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
CacheDb.resetInstance();
} }
// Clear backoff cache in cert scheduler // Clear backoff cache in cert scheduler
@@ -982,6 +983,11 @@ export class DcRouter {
this.apiTokenManager = undefined; this.apiTokenManager = undefined;
this.certificateStatusMap.clear(); this.certificateStatusMap.clear();
// Reset security singletons to allow GC
SecurityLogger.resetInstance();
ContentScanner.resetInstance();
IPReputationChecker.resetInstance();
logger.log('info', 'All DcRouter services stopped'); logger.log('info', 'All DcRouter services stopped');
} catch (error) { } catch (error) {
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) }); logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
@@ -1308,11 +1314,14 @@ export class DcRouter {
} }
// Adaptive logging: individual logs up to 2/sec, then batch // Adaptive logging: individual logs up to 2/sec, then batch
const now = Date.now(); const nowSec = Math.floor(Date.now() / 1000);
this.dnsLogWindow = this.dnsLogWindow.filter(t => now - t < 1000); if (nowSec !== this.dnsLogWindowSecond) {
this.dnsLogWindowSecond = nowSec;
this.dnsLogWindowCount = 0;
}
if (this.dnsLogWindow.length < 2) { if (this.dnsLogWindowCount < 2) {
this.dnsLogWindow.push(now); this.dnsLogWindowCount++;
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', '); const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' }); logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
} else { } else {
@@ -1366,6 +1375,14 @@ export class DcRouter {
return; return;
} }
// Prevent uncaught exception from socket 'error' events
socket.on('error', (err) => {
logger.log('error', `DNS socket error: ${err.message}`);
if (!socket.destroyed) {
socket.destroy();
}
});
logger.log('debug', 'DNS socket handler: passing socket to DnsServer'); logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
try { try {
@@ -1374,8 +1391,10 @@ export class DcRouter {
await (this.dnsServer as any).handleHttpsSocket(socket); await (this.dnsServer as any).handleHttpsSocket(socket);
} catch (error) { } catch (error) {
logger.log('error', `DNS socket handler error: ${error.message}`); logger.log('error', `DNS socket handler error: ${error.message}`);
if (!socket.destroyed) {
socket.destroy(); socket.destroy();
} }
}
}; };
} }
@@ -1580,6 +1599,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
@@ -1590,6 +1610,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');
@@ -1715,10 +1736,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

@@ -122,6 +122,24 @@ export class ApiTokenManager {
return true; return true;
} }
/**
* Roll (regenerate) a token's secret while keeping its identity.
* Returns the new raw token value (shown once).
*/
public async rollToken(id: string): Promise<{ id: string; rawToken: string } | null> {
const stored = this.tokens.get(id);
if (!stored) return null;
const randomBytes = plugins.crypto.randomBytes(32);
const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;
stored.tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
await this.persistToken(stored);
logger.log('info', `API token '${stored.name}' rolled (id: ${id})`);
return { id, rawToken };
}
/** /**
* Enable or disable a token. * Enable or disable a token.
*/ */

View File

@@ -35,7 +35,9 @@ export class MetricsManager {
queryTypes: {} as Record<string, number>, queryTypes: {} as Record<string, number>,
topDomains: new Map<string, number>(), topDomains: new Map<string, number>(),
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
queryTimestamps: [] as number[], // Track query timestamps for rate calculation // Per-second query count ring buffer (300 entries = 5 minutes)
queryRing: new Int32Array(300),
queryRingLastSecond: 0, // last epoch second that was written
responseTimes: [] as number[], // Track response times in ms responseTimes: [] as number[], // Track response times in ms
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>, recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
}; };
@@ -95,7 +97,8 @@ export class MetricsManager {
this.dnsMetrics.cacheMisses = 0; this.dnsMetrics.cacheMisses = 0;
this.dnsMetrics.queryTypes = {}; this.dnsMetrics.queryTypes = {};
this.dnsMetrics.topDomains.clear(); this.dnsMetrics.topDomains.clear();
this.dnsMetrics.queryTimestamps = []; this.dnsMetrics.queryRing.fill(0);
this.dnsMetrics.queryRingLastSecond = 0;
this.dnsMetrics.responseTimes = []; this.dnsMetrics.responseTimes = [];
this.dnsMetrics.recentQueries = []; this.dnsMetrics.recentQueries = [];
this.dnsMetrics.lastResetDate = currentDate; this.dnsMetrics.lastResetDate = currentDate;
@@ -141,16 +144,16 @@ export class MetricsManager {
const smartMetricsData = await this.smartMetrics.getMetrics(); const smartMetricsData = await this.smartMetrics.getMetrics();
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null; const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
return { return {
uptime: process.uptime(), uptime: process.uptime(),
startTime: Date.now() - (process.uptime() * 1000), startTime: Date.now() - (process.uptime() * 1000),
memoryUsage: { memoryUsage: {
heapUsed: process.memoryUsage().heapUsed, heapUsed,
heapTotal: process.memoryUsage().heapTotal, heapTotal,
external: process.memoryUsage().external, external,
rss: process.memoryUsage().rss, rss,
// Add SmartMetrics memory data
maxMemoryMB: this.smartMetrics.maxMemoryMB, maxMemoryMB: this.smartMetrics.maxMemoryMB,
actualUsageBytes: smartMetricsData.memoryUsageBytes, actualUsageBytes: smartMetricsData.memoryUsageBytes,
actualUsagePercentage: smartMetricsData.memoryPercentage, actualUsagePercentage: smartMetricsData.memoryPercentage,
@@ -219,11 +222,8 @@ export class MetricsManager {
.slice(0, 10) .slice(0, 10)
.map(([domain, count]) => ({ domain, count })); .map(([domain, count]) => ({ domain, count }));
// Calculate queries per second from recent timestamps // Calculate queries per second from ring buffer (sum last 60 seconds)
const now = Date.now(); const queriesPerSecond = this.getQueryRingSum(60) / 60;
const oneMinuteAgo = now - 60000;
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
const queriesPerSecond = recentQueries.length / 60;
// Calculate average response time // Calculate average response time
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0 const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
@@ -427,12 +427,8 @@ export class MetricsManager {
this.dnsMetrics.cacheMisses++; this.dnsMetrics.cacheMisses++;
} }
// Track query timestamp // Increment per-second query counter in ring buffer
this.dnsMetrics.queryTimestamps.push(Date.now()); this.incrementQueryRing();
// Keep only timestamps from last 5 minutes
const fiveMinutesAgo = Date.now() - 300000;
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
// Track response time if provided // Track response time if provided
if (responseTimeMs) { if (responseTimeMs) {
@@ -604,7 +600,7 @@ export class MetricsManager {
requestsPerSecond, requestsPerSecond,
requestsTotal, requestsTotal,
}; };
}, 200); // Use 200ms cache for more frequent updates }, 1000); // 1s cache — matches typical dashboard poll interval
} }
// --- Time-series helpers --- // --- Time-series helpers ---
@@ -633,6 +629,63 @@ export class MetricsManager {
bucket.queries++; bucket.queries++;
} }
/**
* Increment the per-second query counter in the ring buffer.
* Zeros any stale slots between the last write and the current second.
*/
private incrementQueryRing(): void {
const currentSecond = Math.floor(Date.now() / 1000);
const ring = this.dnsMetrics.queryRing;
const last = this.dnsMetrics.queryRingLastSecond;
if (last === 0) {
// First call — zero and anchor
ring.fill(0);
this.dnsMetrics.queryRingLastSecond = currentSecond;
ring[currentSecond % ring.length] = 1;
return;
}
const gap = currentSecond - last;
if (gap >= ring.length) {
// Entire ring is stale — clear all
ring.fill(0);
} else if (gap > 0) {
// Zero slots from (last+1) to currentSecond (inclusive)
for (let s = last + 1; s <= currentSecond; s++) {
ring[s % ring.length] = 0;
}
}
this.dnsMetrics.queryRingLastSecond = currentSecond;
ring[currentSecond % ring.length]++;
}
/**
* Sum query counts from the ring buffer for the last N seconds.
*/
private getQueryRingSum(seconds: number): number {
const currentSecond = Math.floor(Date.now() / 1000);
const ring = this.dnsMetrics.queryRing;
const last = this.dnsMetrics.queryRingLastSecond;
if (last === 0) return 0;
// First, zero stale slots so reads are accurate even without writes
const gap = currentSecond - last;
if (gap >= ring.length) return 0; // all data is stale
let sum = 0;
const limit = Math.min(seconds, ring.length);
for (let i = 0; i < limit; i++) {
const sec = currentSecond - i;
if (sec < last - (ring.length - 1)) break; // slot is from older cycle
if (sec > last) continue; // no writes yet for this second
sum += ring[sec % ring.length];
}
return sum;
}
private pruneOldBuckets(): void { private pruneOldBuckets(): void {
const cutoff = Date.now() - 86400000; // 24h const cutoff = Date.now() - 86400000; // 24h
for (const key of this.emailMinuteBuckets.keys()) { for (const key of this.emailMinuteBuckets.keys()) {

View File

@@ -77,6 +77,25 @@ export class ApiTokenHandler {
), ),
); );
// Roll API token
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
'rollApiToken',
async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) {
return { success: false, message: 'Token management not initialized' };
}
const result = await manager.rollToken(dataArg.id);
if (!result) {
return { success: false, message: 'Token not found' };
}
return { success: true, tokenValue: result.rawToken };
},
),
);
// Toggle API token // Toggle API token
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(

View File

@@ -40,11 +40,20 @@ export class ConfigHandler {
? 'filesystem' ? 'filesystem'
: 'memory'; : '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'] = { const system: interfaces.requests.IConfigData['system'] = {
baseDir: resolvedPaths.dcrouterHomeDir, baseDir: resolvedPaths.dcrouterHomeDir,
dataDir: resolvedPaths.dataDir, dataDir: resolvedPaths.dataDir,
publicIp: opts.publicIp || null, publicIp: opts.publicIp || dcRouter.detectedPublicIp || null,
proxyIps: opts.proxyIps || [], proxyIps,
uptime: Math.floor(process.uptime()), uptime: Math.floor(process.uptime()),
storageBackend, storageBackend,
storagePath: opts.storage?.fsPath || null, storagePath: opts.storage?.fsPath || null,
@@ -169,11 +178,27 @@ export class ConfigHandler {
// --- Remote Ingress --- // --- Remote Ingress ---
const riCfg = opts.remoteIngressConfig; const riCfg = opts.remoteIngressConfig;
const connectedEdgeIps = dcRouter.tunnelManager?.getConnectedEdgeIps() || [];
// Determine TLS mode: custom certs > ACME from cert store > self-signed fallback
let tlsMode: 'custom' | 'acme' | 'self-signed' = 'self-signed';
if (riCfg?.tls?.certPath && riCfg?.tls?.keyPath) {
tlsMode = 'custom';
} else if (riCfg?.hubDomain) {
try {
const stored = await dcRouter.storageManager.getJSON(`/proxy-certs/${riCfg.hubDomain}`);
if (stored?.publicKey && stored?.privateKey) {
tlsMode = 'acme';
}
} catch { /* no stored cert */ }
}
const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = { const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
enabled: !!dcRouter.remoteIngressManager, enabled: !!dcRouter.remoteIngressManager,
tunnelPort: riCfg?.tunnelPort || null, tunnelPort: riCfg?.tunnelPort || null,
hubDomain: riCfg?.hubDomain || null, hubDomain: riCfg?.hubDomain || null,
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath), tlsMode,
connectedEdgeIps,
}; };
return { return {

View File

@@ -318,11 +318,15 @@ export class LogsHandler {
try { try {
// Use a timeout to detect hung streams (sendData can hang if the // Use a timeout to detect hung streams (sendData can hang if the
// VirtualStream's keepAlive loop has ended) // VirtualStream's keepAlive loop has ended)
let timeoutHandle: ReturnType<typeof setTimeout>;
await Promise.race([ await Promise.race([
virtualStream.sendData(encoder.encode(logData)), virtualStream.sendData(encoder.encode(logData)).then((result) => {
new Promise<never>((_, reject) => clearTimeout(timeoutHandle);
setTimeout(() => reject(new Error('stream send timeout')), 10_000) return result;
), }),
new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
}),
]); ]);
} catch { } catch {
// Stream closed, errored, or timed out — clean up // Stream closed, errored, or timed out — clean up

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;
};
} }
/** /**
@@ -23,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(),
@@ -62,6 +65,7 @@ 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
@@ -103,12 +107,16 @@ export class TunnelManager {
if (existing) { if (existing) {
existing.activeTunnels = rustEdge.activeStreams; existing.activeTunnels = rustEdge.activeStreams;
existing.lastHeartbeat = Date.now(); existing.lastHeartbeat = Date.now();
// Update peer address if available from Rust hub
if (rustEdge.peerAddr) {
existing.publicIp = rustEdge.peerAddr;
}
} else { } else {
// Missed edgeConnected event — add entry // Missed edgeConnected event — add entry
this.edgeStatuses.set(rustEdge.edgeId, { this.edgeStatuses.set(rustEdge.edgeId, {
edgeId: rustEdge.edgeId, edgeId: rustEdge.edgeId,
connected: true, connected: true,
publicIp: null, publicIp: rustEdge.peerAddr || null,
activeTunnels: rustEdge.activeStreams, activeTunnels: rustEdge.activeStreams,
lastHeartbeat: Date.now(), lastHeartbeat: Date.now(),
connectedAt: rustEdge.connectedAt * 1000, connectedAt: rustEdge.connectedAt * 1000,
@@ -158,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

@@ -183,6 +183,13 @@ export class ContentScanner {
return ContentScanner.instance; return ContentScanner.instance;
} }
/**
* Reset the singleton instance (for shutdown/testing)
*/
public static resetInstance(): void {
ContentScanner.instance = undefined;
}
/** /**
* Scan an email for malicious content * Scan an email for malicious content
* @param email The email to scan * @param email The email to scan

View File

@@ -65,6 +65,8 @@ export class IPReputationChecker {
private reputationCache: LRUCache<string, IReputationResult>; private reputationCache: LRUCache<string, IReputationResult>;
private options: Required<IIPReputationOptions>; private options: Required<IIPReputationOptions>;
private storageManager?: any; // StorageManager instance private storageManager?: any; // StorageManager instance
private saveCacheTimer: ReturnType<typeof setTimeout> | null = null;
private static readonly SAVE_CACHE_DEBOUNCE_MS = 30_000;
// Default DNSBL servers // Default DNSBL servers
private static readonly DEFAULT_DNSBL_SERVERS = [ private static readonly DEFAULT_DNSBL_SERVERS = [
@@ -144,6 +146,19 @@ export class IPReputationChecker {
return IPReputationChecker.instance; return IPReputationChecker.instance;
} }
/**
* Reset the singleton instance (for shutdown/testing)
*/
public static resetInstance(): void {
if (IPReputationChecker.instance) {
if (IPReputationChecker.instance.saveCacheTimer) {
clearTimeout(IPReputationChecker.instance.saveCacheTimer);
IPReputationChecker.instance.saveCacheTimer = null;
}
}
IPReputationChecker.instance = undefined;
}
/** /**
* Check an IP address's reputation * Check an IP address's reputation
* @param ip IP address to check * @param ip IP address to check
@@ -213,12 +228,9 @@ export class IPReputationChecker {
// Update cache with result // Update cache with result
this.reputationCache.set(ip, result); this.reputationCache.set(ip, result);
// Save cache if enabled // Schedule debounced cache save if enabled
if (this.options.enableLocalCache) { if (this.options.enableLocalCache) {
// Fire and forget the save operation this.debouncedSaveCache();
this.saveCache().catch(error => {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
});
} }
// Log the reputation check // Log the reputation check
@@ -447,6 +459,21 @@ export class IPReputationChecker {
}); });
} }
/**
* Schedule a debounced cache save (at most once per SAVE_CACHE_DEBOUNCE_MS)
*/
private debouncedSaveCache(): void {
if (this.saveCacheTimer) {
return; // already scheduled
}
this.saveCacheTimer = setTimeout(() => {
this.saveCacheTimer = null;
this.saveCache().catch(error => {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
});
}, IPReputationChecker.SAVE_CACHE_DEBOUNCE_MS);
}
/** /**
* Save cache to disk or storage manager * Save cache to disk or storage manager
*/ */

View File

@@ -84,6 +84,13 @@ export class SecurityLogger {
return SecurityLogger.instance; return SecurityLogger.instance;
} }
/**
* Reset the singleton instance (for shutdown/testing)
*/
public static resetInstance(): void {
SecurityLogger.instance = undefined;
}
/** /**
* Log a security event * Log a security event
* @param event The security event to log * @param event The security event to log
@@ -155,8 +162,9 @@ export class SecurityLogger {
} }
} }
// Return most recent events up to limit // Return most recent events up to limit (slice first to avoid mutating source)
return filteredEvents return filteredEvents
.slice()
.sort((a, b) => b.timestamp - a.timestamp) .sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit); .slice(0, limit);
} }
@@ -242,40 +250,34 @@ export class SecurityLogger {
topIPs: Array<{ ip: string; count: number }>; topIPs: Array<{ ip: string; count: number }>;
topDomains: Array<{ domain: string; count: number }>; topDomains: Array<{ domain: string; count: number }>;
} { } {
// Filter by time window if provided const cutoff = timeWindow ? Date.now() - timeWindow : 0;
let events = this.securityEvents;
if (timeWindow) { // Initialize counters
const cutoff = Date.now() - timeWindow; const byLevel = {} as Record<SecurityLogLevel, number>;
events = events.filter(e => e.timestamp >= cutoff); for (const level of Object.values(SecurityLogLevel)) {
byLevel[level] = 0;
}
const byType = {} as Record<SecurityEventType, number>;
for (const type of Object.values(SecurityEventType)) {
byType[type] = 0;
} }
// Count by level
const byLevel = Object.values(SecurityLogLevel).reduce((acc, level) => {
acc[level] = events.filter(e => e.level === level).length;
return acc;
}, {} as Record<SecurityLogLevel, number>);
// Count by type
const byType = Object.values(SecurityEventType).reduce((acc, type) => {
acc[type] = events.filter(e => e.type === type).length;
return acc;
}, {} as Record<SecurityEventType, number>);
// Count by IP
const ipCounts = new Map<string, number>(); const ipCounts = new Map<string, number>();
events.forEach(e => { const domainCounts = new Map<string, number>();
// Single pass over all events
let total = 0;
for (const e of this.securityEvents) {
if (cutoff && e.timestamp < cutoff) continue;
total++;
byLevel[e.level]++;
byType[e.type]++;
if (e.ipAddress) { if (e.ipAddress) {
ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1); ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1);
} }
});
// Count by domain
const domainCounts = new Map<string, number>();
events.forEach(e => {
if (e.domain) { if (e.domain) {
domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1); domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
} }
}); }
// Sort and limit top entries // Sort and limit top entries
const topIPs = Array.from(ipCounts.entries()) const topIPs = Array.from(ipCounts.entries())
@@ -288,12 +290,6 @@ export class SecurityLogger {
.sort((a, b) => b.count - a.count) .sort((a, b) => b.count - a.count)
.slice(0, 10); .slice(0, 10);
return { return { total, byLevel, byType, topIPs, topDomains };
total: events.length,
byLevel,
byType,
topIPs,
topDomains
};
} }
} }

View File

@@ -30,6 +30,7 @@ export type StorageBackend = 'filesystem' | 'custom' | 'memory';
* Provides unified key-value storage with multiple backend support * Provides unified key-value storage with multiple backend support
*/ */
export class StorageManager { export class StorageManager {
private static readonly MAX_MEMORY_ENTRIES = 10_000;
private backend: StorageBackend; private backend: StorageBackend;
private memoryStore: Map<string, string> = new Map(); private memoryStore: Map<string, string> = new Map();
private config: IStorageConfig; private config: IStorageConfig;
@@ -227,6 +228,11 @@ export class StorageManager {
case 'memory': { case 'memory': {
this.memoryStore.set(key, value); this.memoryStore.set(key, value);
// Evict oldest entries if memory store exceeds limit
while (this.memoryStore.size > StorageManager.MAX_MEMORY_ENTRIES) {
const firstKey = this.memoryStore.keys().next().value;
this.memoryStore.delete(firstKey);
}
break; break;
} }

View File

@@ -63,6 +63,26 @@ export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.impl
}; };
} }
/**
* Roll (regenerate) an API token's secret. Returns the new raw token value once.
* Admin JWT only.
*/
export interface IReq_RollApiToken extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_RollApiToken
> {
method: 'rollApiToken';
request: {
identity?: authInterfaces.IIdentity;
id: string;
};
response: {
success: boolean;
tokenValue?: string;
message?: string;
};
}
/** /**
* Enable or disable an API token. * Enable or disable an API token.
*/ */

View File

@@ -69,7 +69,8 @@ export interface IConfigData {
enabled: boolean; enabled: boolean;
tunnelPort: number | null; tunnelPort: number | null;
hubDomain: string | null; hubDomain: string | null;
tlsConfigured: boolean; tlsMode: 'custom' | 'acme' | 'self-signed';
connectedEdgeIps: string[];
}; };
} }

View File

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

View File

@@ -581,7 +581,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
}); });
export const reprovisionCertificateAction = certificateStatePart.createAction<string>( export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain) => { async (statePartArg, domain, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -596,8 +596,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
}); });
// Re-fetch overview after reprovisioning // Re-fetch overview after reprovisioning
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null); return await actionContext.dispatch(fetchCertificateOverviewAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -608,7 +607,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
); );
export const deleteCertificateAction = certificateStatePart.createAction<string>( export const deleteCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain) => { async (statePartArg, domain, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -623,8 +622,7 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
}); });
// Re-fetch overview after deletion // Re-fetch overview after deletion
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null); return await actionContext.dispatch(fetchCertificateOverviewAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -643,7 +641,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
publicKey: string; publicKey: string;
csr: string; csr: string;
}>( }>(
async (statePartArg, cert) => { async (statePartArg, cert, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -658,8 +656,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
}); });
// Re-fetch overview after import // Re-fetch overview after import
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null); return await actionContext.dispatch(fetchCertificateOverviewAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -737,7 +734,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[]; listenPorts?: number[];
autoDerivePorts?: boolean; autoDerivePorts?: boolean;
tags?: string[]; tags?: string[];
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -756,7 +753,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
if (response.success) { if (response.success) {
// Refresh the list // Refresh the list
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); await actionContext.dispatch(fetchRemoteIngressAction, null);
return { return {
...statePartArg.getState(), ...statePartArg.getState(),
@@ -774,7 +771,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
}); });
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>( export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
async (statePartArg, edgeId) => { async (statePartArg, edgeId, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -788,8 +785,7 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
id: edgeId, id: edgeId,
}); });
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); return await actionContext.dispatch(fetchRemoteIngressAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -805,7 +801,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[]; listenPorts?: number[];
autoDerivePorts?: boolean; autoDerivePorts?: boolean;
tags?: string[]; tags?: string[];
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -823,8 +819,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
tags: dataArg.tags, tags: dataArg.tags,
}); });
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); return await actionContext.dispatch(fetchRemoteIngressAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -877,7 +872,7 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
id: string; id: string;
enabled: boolean; enabled: boolean;
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -892,8 +887,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
enabled: dataArg.enabled, enabled: dataArg.enabled,
}); });
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); return await actionContext.dispatch(fetchRemoteIngressAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -939,7 +933,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{ export const createRouteAction = routeManagementStatePart.createAction<{
route: any; route: any;
enabled?: boolean; enabled?: boolean;
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -954,8 +948,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled, enabled: dataArg.enabled,
}); });
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); return await actionContext.dispatch(fetchMergedRoutesAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -965,7 +958,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
}); });
export const deleteRouteAction = routeManagementStatePart.createAction<string>( export const deleteRouteAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeId) => { async (statePartArg, routeId, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -979,8 +972,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
id: routeId, id: routeId,
}); });
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); return await actionContext.dispatch(fetchMergedRoutesAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -993,7 +985,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
export const toggleRouteAction = routeManagementStatePart.createAction<{ export const toggleRouteAction = routeManagementStatePart.createAction<{
id: string; id: string;
enabled: boolean; enabled: boolean;
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -1008,8 +1000,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled, enabled: dataArg.enabled,
}); });
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); return await actionContext.dispatch(fetchMergedRoutesAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -1021,7 +1012,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
export const setRouteOverrideAction = routeManagementStatePart.createAction<{ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
routeName: string; routeName: string;
enabled: boolean; enabled: boolean;
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -1036,8 +1027,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled, enabled: dataArg.enabled,
}); });
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); return await actionContext.dispatch(fetchMergedRoutesAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -1047,7 +1037,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
}); });
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>( export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeName) => { async (statePartArg, routeName, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -1061,8 +1051,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
routeName, routeName,
}); });
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null); return await actionContext.dispatch(fetchMergedRoutesAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -1115,8 +1104,20 @@ export async function createApiToken(name: string, scopes: interfaces.data.TApiT
}); });
} }
export async function rollApiToken(id: string) {
const context = getActionContext();
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_RollApiToken
>('/typedrequest', 'rollApiToken');
return request.fire({
identity: context.identity,
id,
});
}
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>( export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
async (statePartArg, tokenId) => { async (statePartArg, tokenId, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -1130,8 +1131,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
id: tokenId, id: tokenId,
}); });
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null); return await actionContext.dispatch(fetchApiTokensAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -1144,7 +1144,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
export const toggleApiTokenAction = routeManagementStatePart.createAction<{ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
id: string; id: string;
enabled: boolean; enabled: boolean;
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
@@ -1159,8 +1159,7 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled, enabled: dataArg.enabled,
}); });
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null); return await actionContext.dispatch(fetchApiTokensAction, null);
return statePartArg.getState();
} catch (error) { } catch (error) {
return { return {
...currentState, ...currentState,
@@ -1321,6 +1320,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

@@ -152,6 +152,15 @@ export class OpsViewApiTokens extends DeesElement {
); );
}, },
}, },
{
name: 'Roll',
iconName: 'lucide:rotateCw',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const token = actionData.item as interfaces.data.IApiTokenInfo;
await this.showRollTokenDialog(token);
},
},
{ {
name: 'Revoke', name: 'Revoke',
iconName: 'lucide:trash2', iconName: 'lucide:trash2',
@@ -279,6 +288,60 @@ export class OpsViewApiTokens extends DeesElement {
}); });
} }
private async showRollTokenDialog(token: interfaces.data.IApiTokenInfo) {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: 'Roll Token Secret',
content: html`
<div style="color: #ccc; padding: 8px 0;">
<p>This will regenerate the secret for <strong>${token.name}</strong>. The old token value will stop working immediately.</p>
</div>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Roll Token',
iconName: 'lucide:rotateCw',
action: async (modalArg: any) => {
await modalArg.destroy();
try {
const response = await appstate.rollApiToken(token.id);
if (response.success && response.tokenValue) {
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
await DeesModal.createAndShow({
heading: 'Token Rolled',
content: html`
<div style="color: #ccc; padding: 8px 0;">
<p>Copy this token now. It will not be shown again.</p>
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
</div>
</div>
`,
menuOptions: [
{
name: 'Done',
iconName: 'lucide:check',
action: async (m: any) => await m.destroy(),
},
],
});
}
} catch (error) {
console.error('Failed to roll token:', error);
}
},
},
],
});
}
async firstUpdated() { async firstUpdated() {
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null); await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
} }

View File

@@ -103,11 +103,20 @@ export class OpsViewConfig extends DeesElement {
} }
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult { private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
// Annotate proxy IPs with source hint when Remote Ingress is active
const ri = this.configState.config?.remoteIngress;
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
);
}
const fields: IConfigField[] = [ const fields: IConfigField[] = [
{ key: 'Base Directory', value: sys.baseDir }, { key: 'Base Directory', value: sys.baseDir },
{ key: 'Data Directory', value: sys.dataDir }, { key: 'Data Directory', value: sys.dataDir },
{ key: 'Public IP', value: sys.publicIp }, { key: 'Public IP', value: sys.publicIp },
{ key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' }, { key: 'Proxy IPs', value: proxyIpValues, type: 'pills' },
{ key: 'Uptime', value: this.formatUptime(sys.uptime) }, { key: 'Uptime', value: this.formatUptime(sys.uptime) },
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' }, { key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
{ key: 'Storage Path', value: sys.storagePath }, { key: 'Storage Path', value: sys.storagePath },
@@ -291,7 +300,8 @@ export class OpsViewConfig extends DeesElement {
const fields: IConfigField[] = [ const fields: IConfigField[] = [
{ key: 'Tunnel Port', value: ri.tunnelPort }, { key: 'Tunnel Port', value: ri.tunnelPort },
{ key: 'Hub Domain', value: ri.hubDomain }, { key: 'Hub Domain', value: ri.hubDomain },
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' }, { key: 'TLS Mode', value: ri.tlsMode, type: 'badge' },
{ key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
]; ];
const actions: IConfigSectionAction[] = [ const actions: IConfigSectionAction[] = [

View File

@@ -76,8 +76,15 @@ export class OpsViewLogs extends DeesElement {
// Wait for xterm terminal to finish initializing (CDN load) // Wait for xterm terminal to finish initializing (CDN load)
if (!chartLog.terminalReady) { if (!chartLog.terminalReady) {
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
let attempts = 0;
const maxAttempts = 200; // 200 * 50ms = 10 seconds
const check = () => { const check = () => {
if (chartLog.terminalReady) { resolve(); return; } if (chartLog.terminalReady) { resolve(); return; }
if (++attempts >= maxAttempts) {
console.warn('ops-view-logs: terminal ready timeout after 10s');
resolve(); // resolve gracefully to avoid blocking
return;
}
setTimeout(check, 50); setTimeout(check, 50);
}; };
check(); check();