Compare commits

...

26 Commits

Author SHA1 Message Date
89b9d01628 v11.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-03-03 21:39:20 +00:00
ed3964e892 BREAKING CHANGE(opsserver): Require authentication for OpsServer endpoints, split handlers into authenticated view/admin routers, and make identity required on many TypedRequest interfaces 2026-03-03 21:39:20 +00:00
baab152fd3 v10.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-03-03 16:19:42 +00:00
9baf09ff61 fix(deps): bump @push.rocks/smartproxy to ^25.9.1 2026-03-03 16:19:42 +00:00
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
43 changed files with 932 additions and 419 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,111 @@
# Changelog # Changelog
## 2026-03-03 - 11.0.0 - BREAKING CHANGE(opsserver)
Require authentication for OpsServer endpoints, split handlers into authenticated view/admin routers, and make identity required on many TypedRequest interfaces
- Added viewRouter and adminRouter to OpsServer and wired middleware to enforce identity/admin checks (requireValidIdentity, requireAdminIdentity).
- Moved handlers to appropriate routers (viewRouter for read endpoints, adminRouter for write/admin endpoints) instead of registering on the unauthenticated main typedrouter.
- Made identity a required field on numerous ts_interfaces request types (breaking change to request typings).
- Refactored ApiTokenHandler to register directly on adminRouter and use dataArg.identity.userId (no per-handler admin checks needed thanks to middleware).
- Updated tests: added admin login to obtain identity, adjusted protected endpoint tests to expect rejection when unauthenticated, and adapted other tests to pass identity where required.
- Added IReq_GetNetworkStats request/response typings to ts_interfaces/requests/stats.ts.
- Bumped dependencies: @api.global/typedrequest ^3.3.0 and @api.global/typedserver ^8.4.2.
## 2026-03-03 - 10.1.9 - fix(deps)
bump @push.rocks/smartproxy to ^25.9.1
- Updated package.json dependency @push.rocks/smartproxy from ^25.9.0 to ^25.9.1
- No other code changes; current package version is 10.1.8, recommend a patch release
## 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) ## 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 expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "9.2.0", "version": "11.0.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -24,12 +24,12 @@
"@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.3.0",
"@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.2",
"@api.global/typedsocket": "^4.1.2", "@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.3", "@design.estate/dees-catalog": "^3.43.3",
@@ -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.3.0", "@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.5", "@push.rocks/smartproxy": "^25.9.1",
"@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@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.2.0", "@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"

196
pnpm-lock.yaml generated
View File

@@ -9,14 +9,14 @@ importers:
.: .:
dependencies: dependencies:
'@api.global/typedrequest': '@api.global/typedrequest':
specifier: ^3.2.6 specifier: ^3.3.0
version: 3.2.6 version: 3.3.0
'@api.global/typedrequest-interfaces': '@api.global/typedrequest-interfaces':
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
'@api.global/typedserver': '@api.global/typedserver':
specifier: ^8.4.0 specifier: ^8.4.2
version: 8.4.0(@tiptap/pm@2.27.2) version: 8.4.2(@tiptap/pm@2.27.2)
'@api.global/typedsocket': '@api.global/typedsocket':
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2(@push.rocks/smartserve@2.0.1) version: 4.1.2(@push.rocks/smartserve@2.0.1)
@@ -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.3.0 specifier: ^5.3.1
version: 5.3.0 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.5 specifier: ^25.9.1
version: 25.8.5 version: 25.9.1
'@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.2.0 specifier: ^4.4.0
version: 4.2.0 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,14 +138,14 @@ 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.3.0':
resolution: {integrity: sha512-CnvbjYjnGGw3rwL+7bTHSgRHEpDujzhs3cv7l1xgCXMPQe3DcPg74+9ep1Y5cu21T/w0pxNnDCJpbb0SHqHzAw==} resolution: {integrity: sha512-Jwobqla+9k2IBG0duwrCFtc6GU6wsvHS3f0gJJsxTrpapylBW1YSF7NnGHPGs7F9hbATsO6IoUBpR2ScoKyGJA==}
'@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.4.0': '@api.global/typedserver@8.4.2':
resolution: {integrity: sha512-qOa5jUwiuHEoY1ZuLZiuU1unPl5JNd99Vv+hNAwgEIgAZd4TTy/mjdTp7lyviBzkGf2pROeCmAbDJJF8YQFCSA==} resolution: {integrity: sha512-eESOcWvrbqkshR4s4OeTX1AK74bNCeGgiRebKgjxIzJ+b0+rkPQyn2DOaMtyXjFZRNgRHyytLm5Iqj5fdazeqw==}
'@api.global/typedsocket@3.1.1': '@api.global/typedsocket@3.1.1':
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==} resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
@@ -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.3.0': '@push.rocks/smartmta@5.3.1':
resolution: {integrity: sha512-uJI25fslzvrcenU36WCdt5gB8cCfkjUlY7PqlxEtFp474/l/kZxNnvirv1gnZLRNNa+ioe5aH18HKE+KcAjuxA==} 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.5': '@push.rocks/smartproxy@25.9.1':
resolution: {integrity: sha512-oLmV+Bq7sSgQP9McTao/imb6Xb62QM7wlTFt5kNynrS5WK2wAe8cEjDKOcyu8N/WmzNCEClT5f/0xAtI6JxtkA==} resolution: {integrity: sha512-X9Yc74CwwvxUay/WDdomRL48vkQyb/kT/QZ4r+AyUUJV2tlt0rQxhbegv9C9kkjrVNsflQWjrAKseh0GB4hAfA==}
'@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==}
@@ -1138,6 +1141,9 @@ packages:
'@push.rocks/webrequest@4.0.2': '@push.rocks/webrequest@4.0.2':
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==} resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
'@push.rocks/webrequest@4.0.5':
resolution: {integrity: sha512-wVSCaXqJ9Vh+rbwVz0wDl46dYz4rnwwSrm5vbVXKbuH6oKTPF0YRoujeJPqRltIn64RVGdLeY9/6ix+ZCrzhsg==}
'@push.rocks/websetup@3.0.19': '@push.rocks/websetup@3.0.19':
resolution: {integrity: sha512-iKJDwXdMmQdu5siOIgziPRxM51lN1AU9HOr+yMteu1YMDkZT7HKCyisDAr4gC9WZ9a7FzsG8zgthm4dMeA8NTw==} resolution: {integrity: sha512-iKJDwXdMmQdu5siOIgziPRxM51lN1AU9HOr+yMteu1YMDkZT7HKCyisDAr4gC9WZ9a7FzsG8zgthm4dMeA8NTw==}
@@ -1344,8 +1350,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.2.0': '@serve.zone/remoteingress@4.4.0':
resolution: {integrity: sha512-VJsWAaTuNGtschbvnDP3PciZ5CGE8KLwrTXC8GN3tcDa2wGj7mJQ+JFlTaQRVoRLtCEMkxMYnLIrhJwal9yEYg==} 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 +1841,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==}
@@ -2081,8 +2087,8 @@ 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: brace-expansion@5.0.4:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
@@ -3244,8 +3250,8 @@ packages:
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@10.2.2: minimatch@10.2.4:
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
minimatch@3.1.3: minimatch@3.1.3:
@@ -4291,21 +4297,21 @@ 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.3.0':
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
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/webrequest': 4.0.2 '@push.rocks/webrequest': 4.0.5
'@push.rocks/webstream': 1.0.10 '@push.rocks/webstream': 1.0.10
'@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.3.0
'@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
@@ -4351,9 +4357,9 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)': '@api.global/typedserver@8.4.2(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.3.0
'@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 +4405,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.3.0
'@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 +4425,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.3.0
'@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 +5000,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.3.0
'@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.3.0
'@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 +5016,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
@@ -5287,7 +5293,7 @@ snapshots:
'@git.zone/tswatch@3.2.0(@tiptap/pm@2.27.2)': '@git.zone/tswatch@3.2.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedserver': 8.4.0(@tiptap/pm@2.27.2) '@api.global/typedserver': 8.4.2(@tiptap/pm@2.27.2)
'@git.zone/tsbundle': 2.9.0 '@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
@@ -5334,7 +5340,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 +5682,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 +5713,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 +5768,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.3.0
'@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 +6182,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 +6250,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartmta@5.3.0': '@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,13 +6357,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.8.5': '@push.rocks/smartproxy@25.9.1':
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.3.1 '@push.rocks/smartrust': 1.3.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
minimatch: 10.2.2 minimatch: 10.2.4
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
@@ -6424,7 +6441,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.3.0
'@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 +6504,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
@@ -6637,6 +6653,14 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
'@push.rocks/webrequest@4.0.5':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 6.0.0
'@push.rocks/smartjson': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20
'@push.rocks/websetup@3.0.19': '@push.rocks/websetup@3.0.19':
dependencies: dependencies:
'@pushrocks/smartdelay': 3.0.1 '@pushrocks/smartdelay': 3.0.1
@@ -6827,7 +6851,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.2.0': '@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 +7383,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 +7406,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 +7419,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 +7451,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 +7479,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 +7516,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 +7561,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': {}
@@ -7718,7 +7742,7 @@ snapshots:
dependencies: dependencies:
balanced-match: 4.0.2 balanced-match: 4.0.2
brace-expansion@5.0.3: brace-expansion@5.0.4:
dependencies: dependencies:
balanced-match: 4.0.4 balanced-match: 4.0.4
@@ -8018,7 +8042,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
@@ -9166,9 +9190,9 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.2 brace-expansion: 5.0.2
minimatch@10.2.2: minimatch@10.2.4:
dependencies: dependencies:
brace-expansion: 5.0.3 brace-expansion: 5.0.4
minimatch@3.1.3: minimatch@3.1.3:
dependencies: dependencies:

View File

@@ -4,27 +4,44 @@ import { TypedRequest } from '@api.global/typedrequest';
import * as interfaces from '../ts_interfaces/index.js'; import * as interfaces from '../ts_interfaces/index.js';
let testDcRouter: DcRouter; let testDcRouter: DcRouter;
let adminIdentity: interfaces.data.IIdentity;
tap.test('should start DCRouter with OpsServer', async () => { tap.test('should start DCRouter with OpsServer', async () => {
testDcRouter = new DcRouter({ testDcRouter = new DcRouter({
// Minimal config for testing // Minimal config for testing
cacheConfig: { enabled: false }, cacheConfig: { enabled: false },
}); });
await testDcRouter.start(); await testDcRouter.start();
expect(testDcRouter.opsServer).toBeInstanceOf(Object); expect(testDcRouter.opsServer).toBeInstanceOf(Object);
}); });
tap.test('should login as admin', async () => {
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
'http://localhost:3000/typedrequest',
'adminLoginWithUsernameAndPassword'
);
const response = await loginRequest.fire({
username: 'admin',
password: 'admin',
});
expect(response).toHaveProperty('identity');
adminIdentity = response.identity;
});
tap.test('should respond to health status request', async () => { tap.test('should respond to health status request', async () => {
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>( const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getHealthStatus' 'getHealthStatus'
); );
const response = await healthRequest.fire({ const response = await healthRequest.fire({
detailed: false identity: adminIdentity,
detailed: false,
}); });
expect(response).toHaveProperty('health'); expect(response).toHaveProperty('health');
expect(response.health.healthy).toBeTrue(); expect(response.health.healthy).toBeTrue();
expect(response.health.services).toHaveProperty('OpsServer'); expect(response.health.services).toHaveProperty('OpsServer');
@@ -35,11 +52,12 @@ tap.test('should respond to server statistics request', async () => {
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getServerStatistics' 'getServerStatistics'
); );
const response = await statsRequest.fire({ const response = await statsRequest.fire({
includeHistory: false identity: adminIdentity,
includeHistory: false,
}); });
expect(response).toHaveProperty('stats'); expect(response).toHaveProperty('stats');
expect(response.stats).toHaveProperty('uptime'); expect(response.stats).toHaveProperty('uptime');
expect(response.stats).toHaveProperty('cpuUsage'); expect(response.stats).toHaveProperty('cpuUsage');
@@ -51,9 +69,11 @@ tap.test('should respond to configuration request', async () => {
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getConfiguration' 'getConfiguration'
); );
const response = await configRequest.fire({}); const response = await configRequest.fire({
identity: adminIdentity,
});
expect(response).toHaveProperty('config'); expect(response).toHaveProperty('config');
expect(response.config).toHaveProperty('system'); expect(response.config).toHaveProperty('system');
expect(response.config).toHaveProperty('smartProxy'); expect(response.config).toHaveProperty('smartProxy');
@@ -70,19 +90,34 @@ tap.test('should handle log retrieval request', async () => {
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getRecentLogs' 'getRecentLogs'
); );
const response = await logsRequest.fire({ const response = await logsRequest.fire({
limit: 10 identity: adminIdentity,
limit: 10,
}); });
expect(response).toHaveProperty('logs'); expect(response).toHaveProperty('logs');
expect(response).toHaveProperty('total'); expect(response).toHaveProperty('total');
expect(response).toHaveProperty('hasMore'); expect(response).toHaveProperty('hasMore');
expect(response.logs).toBeArray(); expect(response.logs).toBeArray();
}); });
tap.test('should reject unauthenticated requests', async () => {
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
'http://localhost:3000/typedrequest',
'getHealthStatus'
);
try {
await healthRequest.fire({} as any);
expect(true).toBeFalse(); // Should not reach here
} catch (error) {
expect(error).toBeTruthy();
}
});
tap.test('should stop DCRouter', async () => { tap.test('should stop DCRouter', async () => {
await testDcRouter.stop(); await testDcRouter.stop();
}); });
export default tap.start(); export default tap.start();

View File

@@ -82,28 +82,31 @@ tap.test('should reject verify identity with invalid JWT', async () => {
} }
}); });
tap.test('should allow access to public endpoints without auth', async () => { tap.test('should reject protected endpoints without auth', async () => {
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>( const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getHealthStatus' 'getHealthStatus'
); );
// No identity provided try {
const response = await healthRequest.fire({}); // No identity provided — should be rejected
await healthRequest.fire({} as any);
expect(response).toHaveProperty('health'); expect(true).toBeFalse(); // Should not reach here
expect(response.health.healthy).toBeTrue(); } catch (error) {
console.log('Public endpoint accessible without auth'); expect(error).toBeTruthy();
console.log('Protected endpoint correctly rejects unauthenticated request');
}
}); });
tap.test('should allow read-only config access', async () => { tap.test('should allow authenticated access to protected endpoints', async () => {
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>( const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
'http://localhost:3000/typedrequest', 'http://localhost:3000/typedrequest',
'getConfiguration' 'getConfiguration'
); );
// Config is read-only and doesn't require auth const response = await configRequest.fire({
const response = await configRequest.fire({}); identity: adminIdentity,
});
expect(response).toHaveProperty('config'); expect(response).toHaveProperty('config');
expect(response.config).toHaveProperty('system'); expect(response.config).toHaveProperty('system');
@@ -114,7 +117,7 @@ tap.test('should allow read-only config access', async () => {
expect(response.config).toHaveProperty('cache'); expect(response.config).toHaveProperty('cache');
expect(response.config).toHaveProperty('radius'); expect(response.config).toHaveProperty('radius');
expect(response.config).toHaveProperty('remoteIngress'); expect(response.config).toHaveProperty('remoteIngress');
console.log('Configuration read successfully'); console.log('Authenticated access to config successful');
}); });
tap.test('should stop DCRouter', async () => { tap.test('should stop DCRouter', async () => {

View File

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

View File

@@ -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 */
@@ -221,7 +222,8 @@ export class DcRouter {
public detectedPublicIp: string | null = null; 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;
@@ -900,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();
@@ -956,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
@@ -979,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) });
@@ -1305,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 {
@@ -1363,15 +1375,25 @@ 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 {
// Use the built-in socket handler from smartdns // Use the built-in socket handler from smartdns
// This handles HTTP/2, DoH protocol, etc. // This handles HTTP/2, DoH protocol, etc.
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}`);
socket.destroy(); if (!socket.destroyed) {
socket.destroy();
}
} }
}; };
} }
@@ -1714,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,12 +97,13 @@ 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;
} }
if (currentDate !== this.securityMetrics.lastResetDate) { if (currentDate !== this.securityMetrics.lastResetDate) {
this.securityMetrics.blockedIPs = 0; this.securityMetrics.blockedIPs = 0;
this.securityMetrics.authFailures = 0; this.securityMetrics.authFailures = 0;
@@ -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

@@ -2,14 +2,20 @@ import type DcRouter from '../classes.dcrouter.js';
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import * as handlers from './handlers/index.js'; import * as handlers from './handlers/index.js';
import * as interfaces from '../../ts_interfaces/index.js';
import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js';
export class OpsServer { export class OpsServer {
public dcRouterRef: DcRouter; public dcRouterRef: DcRouter;
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer; public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
// TypedRouter for OpsServer-specific handlers // Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
// Auth-enforced routers — middleware validates identity before any handler runs
public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
// Handler instances // Handler instances
public adminHandler: handlers.AdminHandler; public adminHandler: handlers.AdminHandler;
private configHandler: handlers.ConfigHandler; private configHandler: handlers.ConfigHandler;
@@ -25,7 +31,7 @@ export class OpsServer {
constructor(dcRouterRefArg: DcRouter) { constructor(dcRouterRefArg: DcRouter) {
this.dcRouterRef = dcRouterRefArg; this.dcRouterRef = dcRouterRefArg;
// Add our typedrouter to the dcRouter's main typedrouter // Add our typedrouter to the dcRouter's main typedrouter
this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter); this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter);
} }
@@ -51,10 +57,25 @@ export class OpsServer {
* Set up all TypedRequest handlers * Set up all TypedRequest handlers
*/ */
private async setupHandlers(): Promise<void> { private async setupHandlers(): Promise<void> {
// Instantiate all handlers - they self-register with the typedrouter // AdminHandler must be initialized first (JWT setup needed for guards)
this.adminHandler = new handlers.AdminHandler(this); this.adminHandler = new handlers.AdminHandler(this);
await this.adminHandler.initialize(); // JWT needs async initialization await this.adminHandler.initialize();
// viewRouter middleware: requires valid identity (any logged-in user)
this.viewRouter.addMiddleware(async (typedRequest) => {
await requireValidIdentity(this.adminHandler, typedRequest.request);
});
// adminRouter middleware: requires admin identity
this.adminRouter.addMiddleware(async (typedRequest) => {
await requireAdminIdentity(this.adminHandler, typedRequest.request);
});
// Connect auth routers to the main typedrouter
this.typedrouter.addTypedRouter(this.viewRouter);
this.typedrouter.addTypedRouter(this.adminRouter);
// Instantiate all handlers — they self-register with the appropriate router
this.configHandler = new handlers.ConfigHandler(this); this.configHandler = new handlers.ConfigHandler(this);
this.logsHandler = new handlers.LogsHandler(this); this.logsHandler = new handlers.LogsHandler(this);
this.securityHandler = new handlers.SecurityHandler(this); this.securityHandler = new handlers.SecurityHandler(this);

View File

@@ -3,34 +3,20 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class ApiTokenHandler { export class ApiTokenHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
/**
* Token management requires admin JWT only (tokens cannot manage tokens).
*/
private async requireAdmin(identity?: interfaces.data.IIdentity): Promise<string> {
if (!identity?.jwt) {
throw new plugins.typedrequest.TypedResponseError('unauthorized');
}
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity });
if (!isAdmin) {
throw new plugins.typedrequest.TypedResponseError('admin access required');
}
return identity.userId;
}
private registerHandlers(): void { private registerHandlers(): void {
// All token management endpoints register directly on adminRouter
// (middleware enforces admin JWT check, so no per-handler requireAdmin needed)
const router = this.opsServerRef.adminRouter;
// Create API token // Create API token
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
'createApiToken', 'createApiToken',
async (dataArg) => { async (dataArg) => {
const userId = await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
@@ -39,7 +25,7 @@ export class ApiTokenHandler {
dataArg.name, dataArg.name,
dataArg.scopes, dataArg.scopes,
dataArg.expiresInDays ?? null, dataArg.expiresInDays ?? null,
userId, dataArg.identity.userId,
); );
return { success: true, tokenId: result.id, tokenValue: result.rawToken }; return { success: true, tokenId: result.id, tokenValue: result.rawToken };
}, },
@@ -47,11 +33,10 @@ export class ApiTokenHandler {
); );
// List API tokens // List API tokens
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
'listApiTokens', 'listApiTokens',
async (dataArg) => { async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { tokens: [] }; return { tokens: [] };
@@ -62,11 +47,10 @@ export class ApiTokenHandler {
); );
// Revoke API token // Revoke API token
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
'revokeApiToken', 'revokeApiToken',
async (dataArg) => { async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };
@@ -77,12 +61,29 @@ export class ApiTokenHandler {
), ),
); );
// Roll API token
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
'rollApiToken',
async (dataArg) => {
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( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
'toggleApiToken', 'toggleApiToken',
async (dataArg) => { async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager; const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) { if (!manager) {
return { success: false, message: 'Token management not initialized' }; return { success: false, message: 'Token management not initialized' };

View File

@@ -3,16 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class CertificateHandler { export class CertificateHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
const viewRouter = this.opsServerRef.viewRouter;
const adminRouter = this.opsServerRef.adminRouter;
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
// Get Certificate Overview // Get Certificate Overview
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
'getCertificateOverview', 'getCertificateOverview',
async (dataArg) => { async (dataArg) => {
@@ -23,8 +25,10 @@ export class CertificateHandler {
) )
); );
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
// Legacy route-based reprovision (backward compat) // Legacy route-based reprovision (backward compat)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
'reprovisionCertificate', 'reprovisionCertificate',
async (dataArg) => { async (dataArg) => {
@@ -34,7 +38,7 @@ export class CertificateHandler {
); );
// Domain-based reprovision (preferred) // Domain-based reprovision (preferred)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
'reprovisionCertificateDomain', 'reprovisionCertificateDomain',
async (dataArg) => { async (dataArg) => {
@@ -44,7 +48,7 @@ export class CertificateHandler {
); );
// Delete certificate // Delete certificate
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
'deleteCertificate', 'deleteCertificate',
async (dataArg) => { async (dataArg) => {
@@ -54,7 +58,7 @@ export class CertificateHandler {
); );
// Export certificate // Export certificate
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
'exportCertificate', 'exportCertificate',
async (dataArg) => { async (dataArg) => {
@@ -64,7 +68,7 @@ export class CertificateHandler {
); );
// Import certificate // Import certificate
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
'importCertificate', 'importCertificate',
async (dataArg) => { async (dataArg) => {

View File

@@ -4,17 +4,16 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class ConfigHandler { export class ConfigHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
// Config endpoint registers directly on viewRouter (valid identity required via middleware)
const router = this.opsServerRef.viewRouter;
// Get Configuration Handler (read-only) // Get Configuration Handler (read-only)
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
'getConfiguration', 'getConfiguration',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -179,11 +178,25 @@ export class ConfigHandler {
// --- Remote Ingress --- // --- Remote Ingress ---
const riCfg = opts.remoteIngressConfig; const riCfg = opts.remoteIngressConfig;
const connectedEdgeIps = dcRouter.tunnelManager?.getConnectedEdgeIps() || []; 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, connectedEdgeIps,
}; };

View File

@@ -3,17 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class EmailOpsHandler { export class EmailOpsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
const viewRouter = this.opsServerRef.viewRouter;
const adminRouter = this.opsServerRef.adminRouter;
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
// Get All Emails Handler // Get All Emails Handler
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
'getAllEmails', 'getAllEmails',
async (dataArg) => { async (dataArg) => {
@@ -24,7 +25,7 @@ export class EmailOpsHandler {
); );
// Get Email Detail Handler // Get Email Detail Handler
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
'getEmailDetail', 'getEmailDetail',
async (dataArg) => { async (dataArg) => {
@@ -34,8 +35,10 @@ export class EmailOpsHandler {
) )
); );
// ---- Write endpoints (adminRouter) ----
// Resend Failed Email Handler // Resend Failed Email Handler
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
'resendEmail', 'resendEmail',
async (dataArg) => { async (dataArg) => {

View File

@@ -10,12 +10,9 @@ let logPushDestinationInstalled = false;
let currentOpsServerRef: OpsServer | null = null; let currentOpsServerRef: OpsServer | null = null;
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
private activeStreamStops: Set<() => void> = new Set(); private activeStreamStops: Set<() => void> = new Set();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
this.setupLogPushDestination(); this.setupLogPushDestination();
} }
@@ -35,8 +32,11 @@ export class LogsHandler {
} }
private registerHandlers(): void { private registerHandlers(): void {
// All log endpoints register directly on viewRouter (valid identity required via middleware)
const router = this.opsServerRef.viewRouter;
// Get Recent Logs Handler // Get Recent Logs Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
'getRecentLogs', 'getRecentLogs',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -59,7 +59,7 @@ export class LogsHandler {
); );
// Get Log Stream Handler // Get Log Stream Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
'getLogStream', 'getLogStream',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -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

@@ -3,21 +3,19 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class RadiusHandler { export class RadiusHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
const viewRouter = this.opsServerRef.viewRouter;
const adminRouter = this.opsServerRef.adminRouter;
// ======================================================================== // ========================================================================
// RADIUS Client Management // RADIUS Client Management
// ======================================================================== // ========================================================================
// Get all RADIUS clients // Get all RADIUS clients (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>(
'getRadiusClients', 'getRadiusClients',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -40,8 +38,8 @@ export class RadiusHandler {
) )
); );
// Add or update a RADIUS client // Add or update a RADIUS client (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>(
'setRadiusClient', 'setRadiusClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -61,8 +59,8 @@ export class RadiusHandler {
) )
); );
// Remove a RADIUS client // Remove a RADIUS client (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>(
'removeRadiusClient', 'removeRadiusClient',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -85,8 +83,8 @@ export class RadiusHandler {
// VLAN Mapping Management // VLAN Mapping Management
// ======================================================================== // ========================================================================
// Get all VLAN mappings // Get all VLAN mappings (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>(
'getVlanMappings', 'getVlanMappings',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -121,8 +119,8 @@ export class RadiusHandler {
) )
); );
// Add or update a VLAN mapping // Add or update a VLAN mapping (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>(
'setVlanMapping', 'setVlanMapping',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -153,8 +151,8 @@ export class RadiusHandler {
) )
); );
// Remove a VLAN mapping // Remove a VLAN mapping (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>(
'removeVlanMapping', 'removeVlanMapping',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -174,8 +172,8 @@ export class RadiusHandler {
) )
); );
// Update VLAN configuration // Update VLAN configuration (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>(
'updateVlanConfig', 'updateVlanConfig',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -206,8 +204,8 @@ export class RadiusHandler {
) )
); );
// Test VLAN assignment // Test VLAN assignment (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>(
'testVlanAssignment', 'testVlanAssignment',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -240,8 +238,8 @@ export class RadiusHandler {
// Accounting / Session Management // Accounting / Session Management
// ======================================================================== // ========================================================================
// Get active sessions // Get active sessions (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>(
'getRadiusSessions', 'getRadiusSessions',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -289,8 +287,8 @@ export class RadiusHandler {
) )
); );
// Disconnect a session // Disconnect a session (write)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>(
'disconnectRadiusSession', 'disconnectRadiusSession',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -314,8 +312,8 @@ export class RadiusHandler {
) )
); );
// Get accounting summary // Get accounting summary (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>(
'getRadiusAccountingSummary', 'getRadiusAccountingSummary',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -351,8 +349,8 @@ export class RadiusHandler {
// Statistics // Statistics
// ======================================================================== // ========================================================================
// Get RADIUS statistics // Get RADIUS statistics (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>(
'getRadiusStatistics', 'getRadiusStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {

View File

@@ -3,16 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
export class RemoteIngressHandler { export class RemoteIngressHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
const viewRouter = this.opsServerRef.viewRouter;
const adminRouter = this.opsServerRef.adminRouter;
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
// Get all remote ingress edges // Get all remote ingress edges
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
'getRemoteIngresses', 'getRemoteIngresses',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -36,8 +38,10 @@ export class RemoteIngressHandler {
), ),
); );
// ---- Write endpoints (adminRouter) ----
// Create a new remote ingress edge // Create a new remote ingress edge
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
'createRemoteIngress', 'createRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -69,7 +73,7 @@ export class RemoteIngressHandler {
); );
// Delete a remote ingress edge // Delete a remote ingress edge
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
'deleteRemoteIngress', 'deleteRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -94,7 +98,7 @@ export class RemoteIngressHandler {
); );
// Update a remote ingress edge // Update a remote ingress edge
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
'updateRemoteIngress', 'updateRemoteIngress',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -138,7 +142,7 @@ export class RemoteIngressHandler {
); );
// Regenerate secret for an edge // Regenerate secret for an edge
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
'regenerateRemoteIngressSecret', 'regenerateRemoteIngressSecret',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -164,8 +168,8 @@ export class RemoteIngressHandler {
), ),
); );
// Get runtime status of all edges // Get runtime status of all edges (read)
this.typedrouter.addTypedHandler( viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
'getRemoteIngressStatus', 'getRemoteIngressStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -178,8 +182,8 @@ export class RemoteIngressHandler {
), ),
); );
// Get a connection token for an edge // Get a connection token for an edge (write — exposes secret)
this.typedrouter.addTypedHandler( adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
'getRemoteIngressConnectionToken', 'getRemoteIngressConnectionToken',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {

View File

@@ -4,17 +4,16 @@ import * as interfaces from '../../../ts_interfaces/index.js';
import { MetricsManager } from '../../monitoring/index.js'; import { MetricsManager } from '../../monitoring/index.js';
export class SecurityHandler { export class SecurityHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
// All security endpoints register directly on viewRouter (valid identity required via middleware)
const router = this.opsServerRef.viewRouter;
// Security Metrics Handler // Security Metrics Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>(
'getSecurityMetrics', 'getSecurityMetrics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -40,7 +39,7 @@ export class SecurityHandler {
); );
// Active Connections Handler // Active Connections Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>(
'getActiveConnections', 'getActiveConnections',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -77,8 +76,8 @@ export class SecurityHandler {
); );
// Network Stats Handler - provides comprehensive network metrics // Network Stats Handler - provides comprehensive network metrics
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>(
'getNetworkStats', 'getNetworkStats',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
// Get network stats from MetricsManager if available // Get network stats from MetricsManager if available
@@ -121,7 +120,7 @@ export class SecurityHandler {
); );
// Rate Limit Status Handler // Rate Limit Status Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
'getRateLimitStatus', 'getRateLimitStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {

View File

@@ -5,17 +5,16 @@ import { MetricsManager } from '../../monitoring/index.js';
import { SecurityLogger } from '../../security/classes.securitylogger.js'; import { SecurityLogger } from '../../security/classes.securitylogger.js';
export class StatsHandler { export class StatsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
} }
private registerHandlers(): void { private registerHandlers(): void {
// All stats endpoints register directly on viewRouter (valid identity required via middleware)
const router = this.opsServerRef.viewRouter;
// Server Statistics Handler // Server Statistics Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>(
'getServerStatistics', 'getServerStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -38,7 +37,7 @@ export class StatsHandler {
); );
// Email Statistics Handler // Email Statistics Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>(
'getEmailStatistics', 'getEmailStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -77,7 +76,7 @@ export class StatsHandler {
); );
// DNS Statistics Handler // DNS Statistics Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>(
'getDnsStatistics', 'getDnsStatistics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -114,7 +113,7 @@ export class StatsHandler {
); );
// Queue Status Handler // Queue Status Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>(
'getQueueStatus', 'getQueueStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -142,7 +141,7 @@ export class StatsHandler {
); );
// Health Status Handler // Health Status Handler
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>(
'getHealthStatus', 'getHealthStatus',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {
@@ -167,7 +166,7 @@ export class StatsHandler {
); );
// Combined Metrics Handler - More efficient for frontend polling // Combined Metrics Handler - More efficient for frontend polling
this.typedrouter.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>( new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
'getCombinedMetrics', 'getCombinedMetrics',
async (dataArg, toolsArg) => { async (dataArg, toolsArg) => {

View File

@@ -22,16 +22,17 @@ export async function passGuards<T extends { identity?: any }>(
} }
/** /**
* Helper to check admin identity in handlers * Helper to check admin identity in handlers and middleware.
* Accepts both optional and required identity for flexibility.
*/ */
export async function requireAdminIdentity<T extends { identity?: interfaces.data.IIdentity }>( export async function requireAdminIdentity(
adminHandler: AdminHandler, adminHandler: AdminHandler,
dataArg: T dataArg: { identity?: interfaces.data.IIdentity }
): Promise<void> { ): Promise<void> {
if (!dataArg.identity) { if (!dataArg.identity) {
throw new plugins.typedrequest.TypedResponseError('No identity provided'); throw new plugins.typedrequest.TypedResponseError('No identity provided');
} }
const passed = await adminHandler.adminIdentityGuard.exec({ identity: dataArg.identity }); const passed = await adminHandler.adminIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) { if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Admin access required'); throw new plugins.typedrequest.TypedResponseError('Admin access required');
@@ -39,16 +40,17 @@ export async function requireAdminIdentity<T extends { identity?: interfaces.dat
} }
/** /**
* Helper to check valid identity in handlers * Helper to check valid identity in handlers and middleware.
* Accepts both optional and required identity for flexibility.
*/ */
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>( export async function requireValidIdentity(
adminHandler: AdminHandler, adminHandler: AdminHandler,
dataArg: T dataArg: { identity?: interfaces.data.IIdentity }
): Promise<void> { ): Promise<void> {
if (!dataArg.identity) { if (!dataArg.identity) {
throw new plugins.typedrequest.TypedResponseError('No identity provided'); throw new plugins.typedrequest.TypedResponseError('No identity provided');
} }
const passed = await adminHandler.validIdentityGuard.exec({ identity: dataArg.identity }); const passed = await adminHandler.validIdentityGuard.exec({ identity: dataArg.identity });
if (!passed) { if (!passed) {
throw new plugins.typedrequest.TypedResponseError('Valid identity required'); throw new plugins.typedrequest.TypedResponseError('Valid identity required');

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;
};
} }
/** /**
@@ -61,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

View File

@@ -182,7 +182,14 @@ 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 = [
@@ -143,7 +145,20 @@ 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

@@ -83,7 +83,14 @@ 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,58 +250,46 @@ 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())
.map(([ip, count]) => ({ ip, count })) .map(([ip, count]) => ({ ip, count }))
.sort((a, b) => b.count - a.count) .sort((a, b) => b.count - a.count)
.slice(0, 10); .slice(0, 10);
const topDomains = Array.from(domainCounts.entries()) const topDomains = Array.from(domainCounts.entries())
.map(([domain, count]) => ({ domain, count })) .map(([domain, count]) => ({ domain, count }))
.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

@@ -16,7 +16,7 @@ export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'createApiToken'; method: 'createApiToken';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
name: string; name: string;
scopes: TApiTokenScope[]; scopes: TApiTokenScope[];
expiresInDays?: number | null; expiresInDays?: number | null;
@@ -38,7 +38,7 @@ export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.imple
> { > {
method: 'listApiTokens'; method: 'listApiTokens';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
tokens: IApiTokenInfo[]; tokens: IApiTokenInfo[];
@@ -54,7 +54,7 @@ export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'revokeApiToken'; method: 'revokeApiToken';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
id: string; id: string;
}; };
response: { response: {
@@ -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.
*/ */
@@ -72,7 +92,7 @@ export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.impl
> { > {
method: 'toggleApiToken'; method: 'toggleApiToken';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
id: string; id: string;
enabled: boolean; enabled: boolean;
}; };

View File

@@ -28,7 +28,7 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
> { > {
method: 'getCertificateOverview'; method: 'getCertificateOverview';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
certificates: ICertificateInfo[]; certificates: ICertificateInfo[];
@@ -50,7 +50,7 @@ export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfa
> { > {
method: 'reprovisionCertificate'; method: 'reprovisionCertificate';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
routeName: string; routeName: string;
}; };
response: { response: {
@@ -66,7 +66,7 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
> { > {
method: 'reprovisionCertificateDomain'; method: 'reprovisionCertificateDomain';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
domain: string; domain: string;
}; };
response: { response: {
@@ -82,7 +82,7 @@ export interface IReq_DeleteCertificate extends plugins.typedrequestInterfaces.i
> { > {
method: 'deleteCertificate'; method: 'deleteCertificate';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
domain: string; domain: string;
}; };
response: { response: {
@@ -98,7 +98,7 @@ export interface IReq_ExportCertificate extends plugins.typedrequestInterfaces.i
> { > {
method: 'exportCertificate'; method: 'exportCertificate';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
domain: string; domain: string;
}; };
response: { response: {
@@ -123,7 +123,7 @@ export interface IReq_ImportCertificate extends plugins.typedrequestInterfaces.i
> { > {
method: 'importCertificate'; method: 'importCertificate';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
cert: { cert: {
id: string; id: string;
domainName: string; domainName: string;

View File

@@ -69,7 +69,7 @@ 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[]; connectedEdgeIps: string[];
}; };
} }
@@ -81,7 +81,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
> { > {
method: 'getConfiguration'; method: 'getConfiguration';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
section?: string; section?: string;
}; };
response: { response: {

View File

@@ -68,7 +68,7 @@ export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implem
> { > {
method: 'getAllEmails'; method: 'getAllEmails';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
emails: IEmail[]; emails: IEmail[];
@@ -84,7 +84,7 @@ export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.impl
> { > {
method: 'getEmailDetail'; method: 'getEmailDetail';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
emailId: string; emailId: string;
}; };
response: { response: {
@@ -101,7 +101,7 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme
> { > {
method: 'resendEmail'; method: 'resendEmail';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
emailId: string; emailId: string;
}; };
response: { response: {

View File

@@ -9,7 +9,7 @@ export interface IReq_GetRecentLogs extends plugins.typedrequestInterfaces.imple
> { > {
method: 'getRecentLogs'; method: 'getRecentLogs';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
level?: 'debug' | 'info' | 'warn' | 'error'; level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email'; category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
limit?: number; limit?: number;
@@ -31,7 +31,7 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
> { > {
method: 'getLogStream'; method: 'getLogStream';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
follow?: boolean; follow?: boolean;
filters?: { filters?: {
level?: string[]; level?: string[];

View File

@@ -14,7 +14,7 @@ export interface IReq_GetRadiusClients extends plugins.typedrequestInterfaces.im
> { > {
method: 'getRadiusClients'; method: 'getRadiusClients';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
clients: Array<{ clients: Array<{
@@ -35,7 +35,7 @@ export interface IReq_SetRadiusClient extends plugins.typedrequestInterfaces.imp
> { > {
method: 'setRadiusClient'; method: 'setRadiusClient';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
client: { client: {
name: string; name: string;
ipRange: string; ipRange: string;
@@ -59,7 +59,7 @@ export interface IReq_RemoveRadiusClient extends plugins.typedrequestInterfaces.
> { > {
method: 'removeRadiusClient'; method: 'removeRadiusClient';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
name: string; name: string;
}; };
response: { response: {
@@ -81,7 +81,7 @@ export interface IReq_GetVlanMappings extends plugins.typedrequestInterfaces.imp
> { > {
method: 'getVlanMappings'; method: 'getVlanMappings';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
mappings: Array<{ mappings: Array<{
@@ -108,7 +108,7 @@ export interface IReq_SetVlanMapping extends plugins.typedrequestInterfaces.impl
> { > {
method: 'setVlanMapping'; method: 'setVlanMapping';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
mapping: { mapping: {
mac: string; mac: string;
vlan: number; vlan: number;
@@ -139,7 +139,7 @@ export interface IReq_RemoveVlanMapping extends plugins.typedrequestInterfaces.i
> { > {
method: 'removeVlanMapping'; method: 'removeVlanMapping';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
mac: string; mac: string;
}; };
response: { response: {
@@ -157,7 +157,7 @@ export interface IReq_UpdateVlanConfig extends plugins.typedrequestInterfaces.im
> { > {
method: 'updateVlanConfig'; method: 'updateVlanConfig';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
defaultVlan?: number; defaultVlan?: number;
allowUnknownMacs?: boolean; allowUnknownMacs?: boolean;
}; };
@@ -179,7 +179,7 @@ export interface IReq_TestVlanAssignment extends plugins.typedrequestInterfaces.
> { > {
method: 'testVlanAssignment'; method: 'testVlanAssignment';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
mac: string; mac: string;
}; };
response: { response: {
@@ -207,7 +207,7 @@ export interface IReq_GetRadiusSessions extends plugins.typedrequestInterfaces.i
> { > {
method: 'getRadiusSessions'; method: 'getRadiusSessions';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
filter?: { filter?: {
username?: string; username?: string;
nasIpAddress?: string; nasIpAddress?: string;
@@ -243,7 +243,7 @@ export interface IReq_DisconnectRadiusSession extends plugins.typedrequestInterf
> { > {
method: 'disconnectRadiusSession'; method: 'disconnectRadiusSession';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
sessionId: string; sessionId: string;
reason?: string; reason?: string;
}; };
@@ -262,7 +262,7 @@ export interface IReq_GetRadiusAccountingSummary extends plugins.typedrequestInt
> { > {
method: 'getRadiusAccountingSummary'; method: 'getRadiusAccountingSummary';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
startTime: number; startTime: number;
endTime: number; endTime: number;
}; };
@@ -296,7 +296,7 @@ export interface IReq_GetRadiusStatistics extends plugins.typedrequestInterfaces
> { > {
method: 'getRadiusStatistics'; method: 'getRadiusStatistics';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
stats: { stats: {

View File

@@ -15,7 +15,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'createRemoteIngress'; method: 'createRemoteIngress';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
name: string; name: string;
listenPorts?: number[]; listenPorts?: number[];
autoDerivePorts?: boolean; autoDerivePorts?: boolean;
@@ -36,7 +36,7 @@ export interface IReq_DeleteRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'deleteRemoteIngress'; method: 'deleteRemoteIngress';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
id: string; id: string;
}; };
response: { response: {
@@ -54,7 +54,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
> { > {
method: 'updateRemoteIngress'; method: 'updateRemoteIngress';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
id: string; id: string;
name?: string; name?: string;
listenPorts?: number[]; listenPorts?: number[];
@@ -77,7 +77,7 @@ export interface IReq_RegenerateRemoteIngressSecret extends plugins.typedrequest
> { > {
method: 'regenerateRemoteIngressSecret'; method: 'regenerateRemoteIngressSecret';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
id: string; id: string;
}; };
response: { response: {
@@ -95,7 +95,7 @@ export interface IReq_GetRemoteIngresses extends plugins.typedrequestInterfaces.
> { > {
method: 'getRemoteIngresses'; method: 'getRemoteIngresses';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
edges: IRemoteIngress[]; edges: IRemoteIngress[];
@@ -111,7 +111,7 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa
> { > {
method: 'getRemoteIngressStatus'; method: 'getRemoteIngressStatus';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
}; };
response: { response: {
statuses: IRemoteIngressStatus[]; statuses: IRemoteIngressStatus[];
@@ -128,7 +128,7 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque
> { > {
method: 'getRemoteIngressConnectionToken'; method: 'getRemoteIngressConnectionToken';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
edgeId: string; edgeId: string;
hubHost?: string; hubHost?: string;
}; };

View File

@@ -9,7 +9,7 @@ export interface IReq_GetServerStatistics extends plugins.typedrequestInterfaces
> { > {
method: 'getServerStatistics'; method: 'getServerStatistics';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
includeHistory?: boolean; includeHistory?: boolean;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
}; };
@@ -29,7 +29,7 @@ export interface IReq_GetEmailStatistics extends plugins.typedrequestInterfaces.
> { > {
method: 'getEmailStatistics'; method: 'getEmailStatistics';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string; domain?: string;
includeDetails?: boolean; includeDetails?: boolean;
@@ -49,7 +49,7 @@ export interface IReq_GetDnsStatistics extends plugins.typedrequestInterfaces.im
> { > {
method: 'getDnsStatistics'; method: 'getDnsStatistics';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string; domain?: string;
includeQueryTypes?: boolean; includeQueryTypes?: boolean;
@@ -69,7 +69,7 @@ export interface IReq_GetRateLimitStatus extends plugins.typedrequestInterfaces.
> { > {
method: 'getRateLimitStatus'; method: 'getRateLimitStatus';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
domain?: string; domain?: string;
ip?: string; ip?: string;
includeBlocked?: boolean; includeBlocked?: boolean;
@@ -91,7 +91,7 @@ export interface IReq_GetSecurityMetrics extends plugins.typedrequestInterfaces.
> { > {
method: 'getSecurityMetrics'; method: 'getSecurityMetrics';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'; timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
includeDetails?: boolean; includeDetails?: boolean;
}; };
@@ -112,7 +112,7 @@ export interface IReq_GetActiveConnections extends plugins.typedrequestInterface
> { > {
method: 'getActiveConnections'; method: 'getActiveConnections';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
protocol?: 'smtp' | 'smtps' | 'http' | 'https'; protocol?: 'smtp' | 'smtps' | 'http' | 'https';
state?: string; state?: string;
}; };
@@ -137,7 +137,7 @@ export interface IReq_GetQueueStatus extends plugins.typedrequestInterfaces.impl
> { > {
method: 'getQueueStatus'; method: 'getQueueStatus';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
queueName?: string; queueName?: string;
}; };
response: { response: {
@@ -153,10 +153,31 @@ export interface IReq_GetHealthStatus extends plugins.typedrequestInterfaces.imp
> { > {
method: 'getHealthStatus'; method: 'getHealthStatus';
request: { request: {
identity?: authInterfaces.IIdentity; identity: authInterfaces.IIdentity;
detailed?: boolean; detailed?: boolean;
}; };
response: { response: {
health: statsInterfaces.IHealthStatus; health: statsInterfaces.IHealthStatus;
}; };
}
// Network Stats (raw SmartProxy network data)
export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetNetworkStats
> {
method: 'getNetworkStats';
request: {
identity: authInterfaces.IIdentity;
};
response: {
connectionsByIP: Array<{ ip: string; count: number }>;
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
topIPs: Array<{ ip: string; count: number }>;
totalDataTransferred: { bytesIn: number; bytesOut: number };
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
throughputByIP: Array<{ ip: string; in: number; out: number }>;
requestsPerSecond: number;
requestsTotal: number;
};
} }

View File

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

View File

@@ -298,8 +298,8 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
// Fetch All Stats Action - Using combined endpoint for efficiency // Fetch All Stats Action - Using combined endpoint for efficiency
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => { export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
// Use combined metrics endpoint - single request instead of 4 // Use combined metrics endpoint - single request instead of 4
@@ -340,8 +340,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
// Fetch Configuration Action (read-only) // Fetch Configuration Action (read-only)
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => { export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -373,6 +373,7 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email'; category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
}>(async (statePartArg, dataArg) => { }>(async (statePartArg, dataArg) => {
const context = getActionContext(); const context = getActionContext();
if (!context.identity) return statePartArg.getState();
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetRecentLogs interfaces.requests.IReq_GetRecentLogs
@@ -448,8 +449,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
// Fetch Network Stats Action // Fetch Network Stats Action
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => { export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
// Fetch active connections using the existing endpoint // Fetch active connections using the existing endpoint
@@ -522,6 +523,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => { export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest< const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -554,6 +556,7 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => { export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest< const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -581,7 +584,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 +599,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 +610,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 +625,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 +644,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 +659,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,
@@ -700,6 +700,7 @@ export async function fetchConnectionToken(edgeId: string) {
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => { export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -737,7 +738,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 +757,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 +775,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 +789,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 +805,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 +823,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 +876,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 +891,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,
@@ -909,6 +907,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => { export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest< const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -939,7 +938,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 +953,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 +963,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 +977,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 +990,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 +1005,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 +1017,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 +1032,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 +1042,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 +1056,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,
@@ -1079,6 +1073,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => { export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try { try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest< const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1115,8 +1110,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 +1137,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 +1150,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 +1165,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,
@@ -1221,8 +1226,9 @@ async function disconnectSocket() {
// Combined refresh action for efficient polling // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();
if (!context.identity) return;
const currentView = uiStatePart.getState().activeView; const currentView = uiStatePart.getState().activeView;
try { try {
// Always fetch basic stats for dashboard widgets // Always fetch basic stats for dashboard widgets
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<

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

@@ -300,7 +300,7 @@ 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' }, { key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
]; ];

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();