Compare commits

...

115 Commits

Author SHA1 Message Date
3a666e9300 v11.0.35
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 15:03:46 +00:00
cbe1b5d37d fix(dev-deps): bump @git.zone/tsbuild devDependency to ^4.1.23 2026-03-05 15:03:45 +00:00
30f2044d9f v11.0.34
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:59:17 +00:00
593b000ca3 fix(dcrouter): empty diff — no changes detected; no version bump suggested 2026-03-05 14:59:17 +00:00
60c298c396 v11.0.33
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:56:27 +00:00
d7f1c16454 fix(build): bump @git.zone/tsbuild to ^4.1.22 2026-03-05 14:56:27 +00:00
4290d4be86 v11.0.32
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:52:57 +00:00
bc34cb5eab fix(dev-deps): bump @git.zone/tsbuild devDependency to ^4.1.21 2026-03-05 14:52:57 +00:00
eda12f3ce3 v11.0.31
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:49:25 +00:00
65f19aac72 fix(deps): bump @git.zone/tsbuild devDependency to ^4.1.20 2026-03-05 14:49:25 +00:00
29a992a695 v11.0.30
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:46:13 +00:00
dbb2166a8f fix(devDependencies): bump @git.zone/tsbuild devDependency to ^4.1.19 2026-03-05 14:46:13 +00:00
22691329a5 v11.0.29
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:42:13 +00:00
e098e1a2ad fix(build): bump @git.zone/tsbuild devDependency to ^4.1.18 2026-03-05 14:42:13 +00:00
16d64ec988 v11.0.28
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-05 14:36:54 +00:00
cb1332ff76 fix(devDependencies): bump @git.zone/tsbuild devDependency to ^4.1.17 2026-03-05 14:36:54 +00:00
3e52060788 v11.0.27
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:32:07 +00:00
f041891a3f fix(deps): bump @git.zone/tsbuild to ^4.1.16 2026-03-05 14:32:07 +00:00
f902c2c1db v11.0.26
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:28:36 +00:00
e1a9e1f997 fix(devDependencies): bump @git.zone/tsbuild devDependency to ^4.1.15 2026-03-05 14:28:36 +00:00
d7b39a3017 v11.0.25
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:13:58 +00:00
0f41b0d8c7 fix(logger): remove build verification comment from logger export 2026-03-05 14:13:58 +00:00
2d33c037ba v11.0.24
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:08:29 +00:00
dca7b37eb8 fix(dcrouter): no changes detected — no release necessary 2026-03-05 14:08:29 +00:00
b56598ba00 v11.0.23
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 14:02:07 +00:00
bbf550b183 fix(deps): bump @git.zone/tsbuild devDependency to ^4.1.14 2026-03-05 14:02:07 +00:00
f4fc5eb1fd v11.0.22
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:47:53 +00:00
d9e88cf5f9 fix(deps): bump @git.zone/tsbuild devDependency to ^4.1.13 2026-03-05 13:47:53 +00:00
eccb9706f2 v11.0.21
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:31:36 +00:00
285e681413 fix(): no changes detected 2026-03-05 13:31:36 +00:00
4f3958d94d v11.0.20
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:29:06 +00:00
d19f22255d fix(logger): annotate singleton logger export comment for build verification 2026-03-05 13:29:06 +00:00
87ec55619a v11.0.19
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:23:20 +00:00
b91dab0f85 fix(dcrouter): no changes 2026-03-05 13:23:20 +00:00
df573d498e v11.0.18
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:19:15 +00:00
da2b838019 fix(dcrouter): no changes detected; no version bump required 2026-03-05 13:19:15 +00:00
107adeee1d v11.0.17
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:15:44 +00:00
45f933b473 fix(dcrouter): no changes detected in diff; no code or documentation updates 2026-03-05 13:15:44 +00:00
ad16bc44f1 v11.0.16
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:12:40 +00:00
96d5b7e01a fix(dcrouter): noop commit: no changes detected 2026-03-05 13:12:40 +00:00
93ffcf86b3 v11.0.15
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:09:29 +00:00
de98b070db fix(): no changes detected; no version bump necessary 2026-03-05 13:09:29 +00:00
d3d2bde440 v11.0.14
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:07:28 +00:00
0840b2b571 fix(dcrouter): no changes detected 2026-03-05 13:07:28 +00:00
fa2e784eaa v11.0.13
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 13:05:13 +00:00
64f2854023 fix(): no code changes 2026-03-05 13:05:13 +00:00
03e3261755 v11.0.12
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 12:56:50 +00:00
c724e68b8c fix(dcrouter): no changes detected — nothing to commit 2026-03-05 12:56:50 +00:00
f8f66d1392 v11.0.11 2026-03-05 12:50:17 +00:00
c66bdc9f88 fix(deps): bump @git.zone/tsbuild devDependency to ^4.1.9 2026-03-05 12:50:17 +00:00
8d57547ace v11.0.10
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 11:35:09 +00:00
54eaf23298 fix(playwright-mcp): remove committed Playwright artifacts and add .playwright-mcp/ to .gitignore 2026-03-05 11:35:09 +00:00
7148306381 v11.0.9
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 10:32:41 +00:00
d3aefef78d fix(devDependencies): bump @git.zone/tsbuild devDependency to ^4.1.4 2026-03-05 10:32:41 +00:00
ecd0cc0066 v11.0.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-05 09:10:14 +00:00
eac490297a fix(): no changes detected 2026-03-05 09:10:14 +00:00
de65641f6f v11.0.7
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-05 08:57:55 +00:00
ffddc1a5f5 fix(deps): bump @git.zone/tsbuild to ^4.1.3 and @push.rocks/lik to ^6.3.1 2026-03-05 08:57:55 +00:00
26152e0520 11.0.6
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-04 07:40:52 +00:00
f79ad07a57 v11.0.5
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Failing after 12m9s
2026-03-04 07:37:12 +00:00
76d5b9bf7c fix(none): no changes detected; nothing to release 2026-03-04 07:37:12 +00:00
670b67eecf v11.0.4
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-04 07:32:50 +00:00
174af5cf86 fix(): no changes 2026-03-04 07:32:50 +00:00
a1f5e45e94 v11.0.3
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-04 07:31:37 +00:00
d06165bd0c fix(): no changes detected 2026-03-04 07:31:37 +00:00
8f3c6fdf23 v11.0.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-04 07:30:26 +00:00
106ef2919e fix(dcrouter): no changes detected; no files were modified 2026-03-04 07:30:26 +00:00
3d7fd233cf v11.0.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-03-04 01:11:19 +00:00
34d40f7370 fix(auth): treat expired JWTs as no identity, improve logout and token verification flow, and bump deps 2026-03-04 01:11:19 +00:00
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
37d62c51f3 v9.2.0
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 23:15:00 +00:00
ea9427d46b feat(remoteingress): expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI 2026-02-26 23:15:00 +00:00
bc77321752 v9.1.10
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 21:34:01 +00:00
65aa546c1c fix(deps): bump @push.rocks/smartproxy to ^25.8.5 2026-02-26 21:34:01 +00:00
54484518dc v9.1.9
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:53:45 +00:00
6fe1247d4d fix(deps(smartmta)): bump @push.rocks/smartmta to ^5.3.0 2026-02-26 17:53:45 +00:00
e59d80a3b3 v9.1.8
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:42:06 +00:00
6c4feba711 fix(deps): bump @serve.zone/remoteingress to ^4.1.0 2026-02-26 17:42:05 +00:00
006a9af20c v9.1.7
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:34:54 +00:00
dfb3b0ac37 fix(dcrouter): bump @push.rocks/smartproxy to ^25.8.4 and remove custom smartProxy timeout/connection lifetime settings from dcrouter 2026-02-26 17:34:54 +00:00
44c1a3a928 v9.1.6
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 17:14:52 +00:00
0c4e28455e fix(cleanup): prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior 2026-02-26 17:14:51 +00:00
cfc4cf378f v9.1.5
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-26 12:49:53 +00:00
a09e69a28b fix(remoteingress): Reconcile tunnel manager edge statuses with authoritative Rust hub periodically; update active tunnel counts and heartbeats, add missed edges, remove stale entries, and clear reconcile interval on stop 2026-02-26 12:49:53 +00:00
82dd19e274 v9.1.4
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-25 00:16:50 +00:00
c1d8afdbf7 fix(deps): bump @push.rocks/smartproxy to ^25.8.1 2026-02-25 00:16:50 +00:00
9b7426f1e6 v9.1.3
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 23:29:26 +00:00
3c9c865841 fix(deps): bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0 2026-02-24 23:29:26 +00:00
8421c9fe46 v9.1.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 20:58:43 +00:00
907e3df156 fix(deps): bump dependency versions for build and runtime packages 2026-02-24 20:58:43 +00:00
53 changed files with 2590 additions and 2343 deletions

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ dist_*/
**/.claude/settings.local.json **/.claude/settings.local.json
.nogit/data/ .nogit/data/
readme.plan.md readme.plan.md
.playwright-mcp/

View File

@@ -1,7 +0,0 @@
[ 74ms] 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)
[ 587ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 697ms] [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

View File

@@ -1,12 +0,0 @@
[ 669ms] [WARNING] Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information. @ http://localhost:3000/chunk-3L5NJTXF.js:13541
[ 729ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/favicon.ico:0
[ 27973ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
[ 27973ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
[ 29975ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
[ 29975ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
[ 33977ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
[ 33978ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
[ 41980ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
[ 41980ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
[ 51983ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
[ 51983ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141

View File

@@ -1,6 +0,0 @@
[ 55ms] 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)
[ 791ms] [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

@@ -1,50 +0,0 @@
[ 272ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078)
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
[ 272ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 274ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Pause-circle
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078)
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
[ 274ms] [WARNING] Lucide icon 'Pause-circle' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 275ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 275ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078)
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 297ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 377ms] [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
[ 78064ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 78237ms] [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
[ 127969ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 127969ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 129695ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 129695ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 133309ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
[ 133309ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
[ 141762ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 141910ms] [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

View File

@@ -1,23 +0,0 @@
[ 437ms] [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
[ 38948ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
[ 52895ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 52896ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 52896ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 52897ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
[ 99401ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
at N.updated (http://localhost:3000/bundle.js:1204:736)
at N._$AE (http://localhost:3000/bundle.js:1:9837)
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
[ 99401ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,5 +1,384 @@
# Changelog # Changelog
## 2026-03-05 - 11.0.35 - fix(dev-deps)
bump @git.zone/tsbuild devDependency to ^4.1.23
- Updated devDependency @git.zone/tsbuild from ^4.1.22 to ^4.1.23 in package.json
## 2026-03-05 - 11.0.34 - fix(dcrouter)
empty diff — no changes detected; no version bump suggested
- No file changes in the provided git diff
- Current package.json version is 11.0.33 — keep unchanged
## 2026-03-05 - 11.0.33 - fix(build)
bump @git.zone/tsbuild to ^4.1.22
- Updated devDependency @git.zone/tsbuild from ^4.1.21 to ^4.1.22
- Change affects build tooling only (devDependencies) — no runtime or API changes expected
## 2026-03-05 - 11.0.32 - fix(dev-deps)
bump @git.zone/tsbuild devDependency to ^4.1.21
- Updated package.json devDependency @git.zone/tsbuild from ^4.1.20 to ^4.1.21
- Change affects development tooling only (no runtime/source changes)
- Bump package patch version from 11.0.31 to 11.0.32 recommended
## 2026-03-05 - 11.0.31 - fix(deps)
bump @git.zone/tsbuild devDependency to ^4.1.20
- Updated devDependency @git.zone/tsbuild from ^4.1.19 to ^4.1.20
## 2026-03-05 - 11.0.30 - fix(devDependencies)
bump @git.zone/tsbuild devDependency to ^4.1.19
- Updated @git.zone/tsbuild from ^4.1.18 to ^4.1.19 in package.json
- Change is limited to devDependencies (build toolchain) and should not affect runtime behavior
## 2026-03-05 - 11.0.29 - fix(build)
bump @git.zone/tsbuild devDependency to ^4.1.18
- Updated @git.zone/tsbuild from ^4.1.17 to ^4.1.18
- Change is a devDependency update only; no runtime behavior expected to change
- Recommend patch version bump
## 2026-03-05 - 11.0.28 - fix(devDependencies)
bump @git.zone/tsbuild devDependency to ^4.1.17
- package.json: updated @git.zone/tsbuild from ^4.1.16 to ^4.1.17 (devDependency)
## 2026-03-05 - 11.0.27 - fix(deps)
bump @git.zone/tsbuild to ^4.1.16
- Updated devDependency @git.zone/tsbuild from ^4.1.15 to ^4.1.16 in package.json
- No runtime code or dependency changes; only a dev/build tool bump
## 2026-03-05 - 11.0.26 - fix(devDependencies)
bump @git.zone/tsbuild devDependency to ^4.1.15
- Updated devDependency @git.zone/tsbuild from ^4.1.14 to ^4.1.15 in package.json
- No runtime changes; development tooling update only
## 2026-03-05 - 11.0.25 - fix(logger)
remove build verification comment from logger export
- Removed parenthetical '(build verification)' from export comment in ts/logger.ts
- No functional changes — this is a comment-only cleanup
## 2026-03-05 - 11.0.24 - fix(dcrouter)
no changes detected — no release necessary
- No files changed in the provided diff; no code, docs, or dependency updates to release.
## 2026-03-05 - 11.0.23 - fix(deps)
bump @git.zone/tsbuild devDependency to ^4.1.14
- Updated devDependency @git.zone/tsbuild from ^4.1.13 to ^4.1.14 in package.json
- Change affects build tooling only (devDependencies); no production code changes
## 2026-03-05 - 11.0.22 - fix(deps)
bump @git.zone/tsbuild devDependency to ^4.1.13
- Updated @git.zone/tsbuild from ^4.1.9 to ^4.1.13 in devDependencies
- No runtime code changes; build/dev dependency update only
## 2026-03-05 - 11.0.21 - fix()
no changes detected
- No files changed in this diff; no release required.
## 2026-03-05 - 11.0.20 - fix(logger)
annotate singleton logger export comment for build verification
- Changed comment in ts/logger.ts to add '(build verification)'
- No functional code changes; only a comment update
- Intended to mark the export for build verification purposes
## 2026-03-05 - 11.0.19 - fix(dcrouter)
no changes
- No files changed in this commit.
- Package version remains 11.0.18.
## 2026-03-05 - 11.0.18 - fix(dcrouter)
no changes detected; no version bump required
- Git diff contains no changes — nothing to release
## 2026-03-05 - 11.0.17 - fix(dcrouter)
no changes detected in diff; no code or documentation updates
- No files changed in this diff
- No code, tests, or documentation modified; no release required
## 2026-03-05 - 11.0.16 - fix(dcrouter)
noop commit: no changes detected
- No files changed in this diff.
- No code or configuration modifications detected.
## 2026-03-05 - 11.0.15 - fix()
no changes detected; no version bump necessary
- Diff contains no changes; no files were modified
## 2026-03-05 - 11.0.14 - fix(dcrouter)
no changes detected
- Provided git diff contains no changes; nothing to release or bump
- Create a commit only if an empty/placeholder commit is intentionally required
## 2026-03-05 - 11.0.13 - fix()
no code changes
- No files were changed in this commit.
## 2026-03-05 - 11.0.12 - fix(dcrouter)
no changes detected — nothing to commit
- Diff reported: No changes
- No files were modified or staged; no functional or documentation changes to release
## 2026-03-05 - 11.0.11 - fix(deps)
bump @git.zone/tsbuild devDependency to ^4.1.9
- Updated @git.zone/tsbuild from ^4.1.4 to ^4.1.9 in package.json
## 2026-03-05 - 11.0.10 - fix(playwright-mcp)
remove committed Playwright artifacts and add .playwright-mcp/ to .gitignore
- Added .playwright-mcp/ to .gitignore to avoid committing transient Playwright outputs
- Removed many Playwright-generated logs, screenshots and console dumps under .playwright-mcp/ to reduce repository noise/size
- Prevents accidental check-in of large test artifacts generated by Playwright runs
## 2026-03-05 - 11.0.9 - fix(devDependencies)
bump @git.zone/tsbuild devDependency to ^4.1.4
- package.json: Updated @git.zone/tsbuild from ^4.1.3 to ^4.1.4
## 2026-03-05 - 11.0.8 - fix()
no changes detected
- No files changed in this commit
- No version bump recommended
## 2026-03-05 - 11.0.7 - fix(deps)
bump @git.zone/tsbuild to ^4.1.3 and @push.rocks/lik to ^6.3.1
- Updated devDependency @git.zone/tsbuild from ^4.1.2 to ^4.1.3 in package.json
- Updated dependency @push.rocks/lik from ^6.2.2 to ^6.3.1 in package.json
- Changes are non-breaking dependency bumps; no source code changes
## 2026-03-04 - 11.0.5 - fix(none)
no changes detected; nothing to release
- Diff contained no changes
- No files modified; no version bump required
## 2026-03-04 - 11.0.4 - fix()
no changes
- No files changed in the provided diff; no release or version bump required.
## 2026-03-04 - 11.0.3 - fix()
no changes detected
- Diff shows no file changes; no code changes to release.
## 2026-03-04 - 11.0.2 - fix(dcrouter)
no changes detected; no files were modified
- diff was empty
- no source or package changes detected
## 2026-03-04 - 11.0.1 - fix(auth)
treat expired JWTs as no identity, improve logout and token verification flow, and bump deps
- App: getActionContext now treats expired JWTs as null to avoid using stale identities for requests.
- Logout action always clears local login state; server-side adminLogout is attempted only when a valid identity exists.
- Dashboard: verify persisted JWT with server (verifyIdentity) on startup; if verification fails, clear state and show login.
- Auto-refresh: on combined refresh failure, detect auth-related errors (invalid/unauthorized/401), dispatch logout and reload to force re-login.
- Deps: bumped devDependencies @git.zone/tstest (^3.2.0) and @git.zone/tswatch (^3.2.5); added runtime dependency @push.rocks/lik (^6.2.2).
- Tests/artifacts: added Playwright console logs and page screenshots (test artifacts) to the commit.
## 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)
expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI
- Add detectedPublicIp to DC Router and populate it when a configured or auto-discovered public IP is chosen
- Use dcRouter.detectedPublicIp as a fallback for system.publicIp in the config handler
- Resolve proxy IPs from SmartProxy runtime settings when opts.proxyIps is not provided
- TunnelManager: capture peerAddr on edgeConnected and from Rust heartbeats, store per-edge publicIp, and add getConnectedEdgeIps()
- Expose connectedEdgeIps in the config API and return it in remoteIngress config
- Ops UI: show Connected Edge IPs, annotate 127.0.0.1 proxy IP as 'Remote Ingress' when applicable, and refresh remote ingress data during combined refresh when viewing remoteingress
- Bump dependency @serve.zone/remoteingress to ^4.2.0
## 2026-02-26 - 9.1.10 - fix(deps)
bump @push.rocks/smartproxy to ^25.8.5
- package.json: @push.rocks/smartproxy version updated from ^25.8.4 to ^25.8.5
- No other files changed
## 2026-02-26 - 9.1.9 - fix(deps(smartmta))
bump @push.rocks/smartmta to ^5.3.0
- Updated @push.rocks/smartmta from ^5.2.6 to ^5.3.0 in package.json
- Patch release recommended (no source code changes)
## 2026-02-26 - 9.1.8 - fix(deps)
bump @serve.zone/remoteingress to ^4.1.0
- Updated dependency @serve.zone/remoteingress from ^4.0.1 to ^4.1.0 in package.json
- Non-breaking dependency update; recommend patch version bump
## 2026-02-26 - 9.1.7 - fix(dcrouter)
bump @push.rocks/smartproxy to ^25.8.4 and remove custom smartProxy timeout/connection lifetime settings from dcrouter
- Bumped dependency @push.rocks/smartproxy from ^25.8.3 to ^25.8.4 in package.json
- Removed explicit smartProxy options: socketTimeout, inactivityTimeout, keepAliveInactivityMultiplier, extendedKeepAliveLifetime, and maxConnectionLifetime from ts/classes.dcrouter.ts
## 2026-02-26 - 9.1.6 - fix(cleanup)
prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior
- Tightened smartProxy connection timeouts and lifetimes (5m socketTimeout, 10m inactivityTimeout, keep-alive multiplier, 1h extendedKeepAliveLifetime, 4h maxConnectionLifetime).
- Remove event listeners before stopping services to avoid leaks (smartProxy, emailServer, dnsServer, remote ingress hub).
- OpsServer.stop now invokes logsHandler.cleanup to tear down active log streams and avoid duplicate push destinations.
- LogsHandler rewritten to use a module-level singleton push destination, track active stream stop callbacks, add cleanup(), guard against hung VirtualStream.sendData with a 10s timeout, and ensure intervals are cleared on stop.
- updateSmartProxyConfig removes listeners on the old instance before stopping it.
- Dependency bumps: @api.global/typedsocket ^4.1.2, @push.rocks/smartdata ^7.1.0, @push.rocks/smartmta ^5.2.6, @push.rocks/smartproxy ^25.8.3.
## 2026-02-26 - 9.1.5 - fix(remoteingress)
Reconcile tunnel manager edge statuses with authoritative Rust hub periodically; update active tunnel counts and heartbeats, add missed edges, remove stale entries, and clear reconcile interval on stop
- Add reconcile() to sync TS-side edgeStatuses with hub.getStatus and overwrite activeTunnels with the authoritative activeStreams.
- Start a periodic reconcile (setInterval every 15s) and store the interval handle on the tunnel manager.
- Clear the reconcile interval in stop() to avoid background timers; remove edgeStatuses entries that are no longer connected in Rust.
- Bump dependency @serve.zone/remoteingress from ^4.0.0 to ^4.0.1.
## 2026-02-25 - 9.1.4 - fix(deps)
bump @push.rocks/smartproxy to ^25.8.1
- Updated package.json dependency @push.rocks/smartproxy from ^25.8.0 to ^25.8.1
## 2026-02-24 - 9.1.3 - fix(deps)
bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0
- Updated @api.global/typedserver from ^8.3.1 to ^8.4.0
- Updated @push.rocks/smartproxy from ^25.7.9 to ^25.8.0
## 2026-02-24 - 9.1.2 - fix(deps)
bump dependency versions for build and runtime packages
- @git.zone/tsbundle: ^2.8.3 -> ^2.9.0
- @git.zone/tswatch: ^3.1.0 -> ^3.2.0
- @api.global/typedserver: ^8.3.0 -> ^8.3.1
- @design.estate/dees-catalog: ^3.43.2 -> ^3.43.3
## 2026-02-23 - 9.1.1 - fix(dcrouter) ## 2026-02-23 - 9.1.1 - fix(dcrouter)
no changes detected — no files modified, no release necessary no changes detected — no files modified, no release necessary

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "9.1.1", "version": "11.0.35",
"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": {
@@ -19,45 +19,46 @@
"watch": "tswatch" "watch": "tswatch"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.1.23",
"@git.zone/tsbundle": "^2.8.3", "@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.2.0",
"@git.zone/tswatch": "^3.1.0", "@git.zone/tswatch": "^3.2.5",
"@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.3.0", "@api.global/typedserver": "^8.4.2",
"@api.global/typedsocket": "^4.1.0", "@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.2", "@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/lik": "^6.3.1",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartacme": "^9.1.3",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdata": "^7.1.0",
"@push.rocks/smartdns": "^7.9.0", "@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.1", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.1", "@push.rocks/smartmetrics": "^3.0.2",
"@push.rocks/smartmongo": "^5.1.0", "@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.2.2", "@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.7.9", "@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.0.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"

2973
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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.1.1', version: '11.0.35',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -23,6 +23,7 @@ import { MetricsManager } from './monitoring/index.js';
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js'; import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js'; import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { RouteConfigManager, ApiTokenManager } from './config/index.js'; import { RouteConfigManager, ApiTokenManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
export interface IDcRouterOptions { export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -217,8 +218,12 @@ export class DcRouter {
public routeConfigManager?: RouteConfigManager; public routeConfigManager?: RouteConfigManager;
public apiTokenManager?: ApiTokenManager; public apiTokenManager?: ApiTokenManager;
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
public detectedPublicIp: string | null = null;
// DNS query logging rate limiter state // DNS query logging rate limiter state
private dnsLogWindow: number[] = []; private dnsLogWindowSecond: number = 0; // epoch second of current window
private dnsLogWindowCount: number = 0; // queries logged this second
private dnsBatchCount: number = 0; private dnsBatchCount: number = 0;
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null; private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
@@ -897,12 +902,27 @@ 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();
try { try {
// Remove event listeners before stopping services to prevent leaks
if (this.smartProxy) {
this.smartProxy.removeAllListeners();
}
if (this.emailServer) {
if ((this.emailServer as any).deliverySystem) {
(this.emailServer as any).deliverySystem.removeAllListeners();
}
this.emailServer.removeAllListeners();
}
if (this.dnsServer) {
this.dnsServer.removeAllListeners();
}
// Stop all services in parallel for faster shutdown // Stop all services in parallel for faster shutdown
await Promise.all([ await Promise.all([
// Stop cache cleaner if running // Stop cache cleaner if running
@@ -939,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
@@ -962,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) });
@@ -976,10 +1002,11 @@ export class DcRouter {
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> { public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
// Stop existing SmartProxy if running // Stop existing SmartProxy if running
if (this.smartProxy) { if (this.smartProxy) {
this.smartProxy.removeAllListeners();
await this.smartProxy.stop(); await this.smartProxy.stop();
this.smartProxy = undefined; this.smartProxy = undefined;
} }
// Update configuration // Update configuration
this.options.smartProxyConfig = config; this.options.smartProxyConfig = config;
@@ -1103,6 +1130,11 @@ export class DcRouter {
try { try {
// Stop the unified email server which contains all components // Stop the unified email server which contains all components
if (this.emailServer) { if (this.emailServer) {
// Remove listeners before stopping to prevent leaks on config update cycles
if ((this.emailServer as any).deliverySystem) {
(this.emailServer as any).deliverySystem.removeAllListeners();
}
this.emailServer.removeAllListeners();
await this.emailServer.stop(); await this.emailServer.stop();
logger.log('info', 'Unified email server stopped'); logger.log('info', 'Unified email server stopped');
this.emailServer = undefined; this.emailServer = undefined;
@@ -1282,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 {
@@ -1340,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();
}
} }
}; };
} }
@@ -1554,6 +1599,7 @@ export class DcRouter {
} else if (this.options.publicIp) { } else if (this.options.publicIp) {
// Use explicitly configured public IP // Use explicitly configured public IP
publicIp = this.options.publicIp; publicIp = this.options.publicIp;
this.detectedPublicIp = publicIp;
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`); logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
} else { } else {
// Auto-discover public IP using smartnetwork // Auto-discover public IP using smartnetwork
@@ -1564,6 +1610,7 @@ export class DcRouter {
if (publicIps.v4) { if (publicIps.v4) {
publicIp = publicIps.v4; publicIp = publicIps.v4;
this.detectedPublicIp = publicIp;
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`); logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
} else { } else {
logger.log('warn', 'Could not auto-discover public IPv4 address'); logger.log('warn', 'Could not auto-discover public IPv4 address');
@@ -1689,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);
@@ -70,6 +91,10 @@ export class OpsServer {
} }
public async stop() { public async stop() {
// Clean up log handler streams and push destination before stopping the server
if (this.logsHandler) {
this.logsHandler.cleanup();
}
if (this.server) { if (this.server) {
await this.server.stop(); await this.server.stop();
} }

View File

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

View File

@@ -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

@@ -3,19 +3,40 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { logBuffer, baseLogger } from '../../logger.js'; import { logBuffer, baseLogger } from '../../logger.js';
// Module-level singleton: the log push destination is added once and reuses
// the current OpsServer reference so it survives OpsServer restarts without
// accumulating duplicate destinations.
let logPushDestinationInstalled = false;
let currentOpsServerRef: OpsServer | null = null;
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); 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();
} }
/**
* Clean up all active log streams and deactivate the push destination.
* Called when OpsServer stops.
*/
public cleanup(): void {
// Stop all active follow-mode log streams
for (const stop of this.activeStreamStops) {
stop();
}
this.activeStreamStops.clear();
// Deactivate the push destination (it stays registered but becomes a no-op)
currentOpsServerRef = null;
}
private registerHandlers(): void { private registerHandlers(): void {
// 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) => {
@@ -27,24 +48,24 @@ export class LogsHandler {
dataArg.search, dataArg.search,
dataArg.timeRange dataArg.timeRange
); );
return { return {
logs, logs,
total: logs.length, // TODO: Implement proper total count total: logs.length,
hasMore: false, // TODO: Implement proper pagination hasMore: false,
}; };
} }
) )
); );
// 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) => {
// Create a virtual stream for log streaming // Create a virtual stream for log streaming
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>(); const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
// Set up log streaming // Set up log streaming
const streamLogs = this.setupLogStream( const streamLogs = this.setupLogStream(
virtualStream, virtualStream,
@@ -52,20 +73,21 @@ export class LogsHandler {
dataArg.filters?.category, dataArg.filters?.category,
dataArg.follow dataArg.follow
); );
// Start streaming // Start streaming
streamLogs.start(); streamLogs.start();
// VirtualStream handles cleanup automatically // Track the stop function so we can clean up on shutdown
this.activeStreamStops.add(streamLogs.stop);
return { return {
logStream: virtualStream as any, // Cast to IVirtualStream interface logStream: virtualStream as any,
}; };
} }
) )
); );
} }
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' { private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
switch (smartlogLevel) { switch (smartlogLevel) {
case 'silly': case 'silly':
@@ -165,18 +187,30 @@ export class LogsHandler {
return mapped; return mapped;
} }
/** /**
* Add a log destination to the base logger that pushes entries * Add a log destination to the base logger that pushes entries
* to all connected ops_dashboard TypedSocket clients. * to all connected ops_dashboard TypedSocket clients.
*
* Uses a module-level singleton so the destination is added only once,
* even across OpsServer restart cycles. The destination reads
* `currentOpsServerRef` dynamically so it always uses the active server.
*/ */
private setupLogPushDestination(): void { private setupLogPushDestination(): void {
const opsServerRef = this.opsServerRef; // Update the module-level reference so the existing destination uses the new server
currentOpsServerRef = this.opsServerRef;
if (logPushDestinationInstalled) {
return; // destination already registered — just updated the ref
}
logPushDestinationInstalled = true;
baseLogger.addLogDestination({ baseLogger.addLogDestination({
async handleLog(logPackage: any) { async handleLog(logPackage: any) {
// Access the TypedSocket server instance from OpsServer const opsServer = currentOpsServerRef;
const typedsocket = opsServerRef.server?.typedserver?.typedsocket; if (!opsServer) return;
const typedsocket = opsServer.server?.typedserver?.typedsocket;
if (!typedsocket) return; if (!typedsocket) return;
let connections: any[]; let connections: any[];
@@ -220,8 +254,18 @@ export class LogsHandler {
stop: () => void; stop: () => void;
} { } {
let intervalId: NodeJS.Timeout | null = null; let intervalId: NodeJS.Timeout | null = null;
let stopped = false;
let logIndex = 0; let logIndex = 0;
const stop = () => {
stopped = true;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
this.activeStreamStops.delete(stop);
};
const start = () => { const start = () => {
if (!follow) { if (!follow) {
// Send existing logs and close // Send existing logs and close
@@ -236,13 +280,19 @@ export class LogsHandler {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
virtualStream.sendData(encoder.encode(logData)); virtualStream.sendData(encoder.encode(logData));
}); });
// VirtualStream doesn't have end() method - it closes automatically
}); });
return; return;
} }
// For follow mode, simulate real-time log streaming // For follow mode, simulate real-time log streaming
intervalId = setInterval(async () => { intervalId = setInterval(async () => {
if (stopped) {
// Guard: clear interval if stop() was called between ticks
clearInterval(intervalId!);
intervalId = null;
return;
}
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
@@ -266,30 +316,25 @@ export class LogsHandler {
const logData = JSON.stringify(logEntry); const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
try { try {
await virtualStream.sendData(encoder.encode(logData)); // Use a timeout to detect hung streams (sendData can hang if the
// VirtualStream's keepAlive loop has ended)
let timeoutHandle: ReturnType<typeof setTimeout>;
await Promise.race([
virtualStream.sendData(encoder.encode(logData)).then((result) => {
clearTimeout(timeoutHandle);
return result;
}),
new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
}),
]);
} catch { } catch {
// Stream closed or errored — clean up to prevent interval leak // Stream closed, errored, or timed out — clean up
clearInterval(intervalId!); stop();
intervalId = null;
} }
}, 2000); // Send a log every 2 seconds }, 2000);
// TODO: Hook into actual logger events
// logger.on('log', (logEntry) => {
// if (matchesCriteria(logEntry, level, service)) {
// virtualStream.sendData(formatLogEntry(logEntry));
// }
// });
}; };
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
// TODO: Unhook from logger events
};
return { start, stop }; return { start, stop };
} }
} }

View File

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

View File

@@ -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,8 @@ export interface IConfigData {
enabled: boolean; enabled: boolean;
tunnelPort: number | null; tunnelPort: number | null;
hubDomain: string | null; hubDomain: string | null;
tlsConfigured: boolean; tlsMode: 'custom' | 'acme' | 'self-signed';
connectedEdgeIps: string[];
}; };
} }
@@ -80,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.1.1', version: '11.0.35',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -238,9 +238,12 @@ interface IActionContext {
} }
const getActionContext = (): IActionContext => { const getActionContext = (): IActionContext => {
return { const identity = loginStatePart.getState().identity;
identity: loginStatePart.getState().identity, // Treat expired JWTs as no identity — prevents stale persisted sessions from firing requests
}; if (identity && identity.expiresAt && identity.expiresAt < Date.now()) {
return { identity: null };
}
return { identity };
}; };
// Login Action // Login Action
@@ -271,24 +274,23 @@ export const loginAction = loginStatePart.createAction<{
} }
}); });
// Logout Action // Logout Action — always clears state, even if identity is expired/missing
export const logoutAction = loginStatePart.createAction(async (statePartArg) => { export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
const context = getActionContext(); const context = getActionContext();
if (!context.identity) return statePartArg.getState();
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< // Try to notify server, but don't block logout if identity is missing/expired
interfaces.requests.IReq_AdminLogout if (context.identity) {
>('/typedrequest', 'adminLogout'); const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_AdminLogout
try { >('/typedrequest', 'adminLogout');
await typedRequest.fire({ try {
identity: context.identity, await typedRequest.fire({ identity: context.identity });
}); } catch (error) {
} catch (error) { console.error('Logout error:', error);
console.error('Logout error:', error); }
} }
// Clear login state regardless // Always clear login state
return { return {
identity: null, identity: null,
isLoggedIn: false, isLoggedIn: false,
@@ -298,8 +300,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 +342,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 +375,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 +451,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 +525,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 +558,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 +586,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 +601,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 +612,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 +627,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 +646,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 +661,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 +702,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 +740,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 +759,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 +777,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 +791,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 +807,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 +825,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 +878,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 +893,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 +909,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 +940,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 +955,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 +965,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 +979,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 +992,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 +1007,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 +1019,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 +1034,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 +1044,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 +1058,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 +1075,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 +1112,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 +1139,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 +1152,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 +1167,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 +1228,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<
@@ -1321,8 +1329,23 @@ async function dispatchCombinedRefreshAction() {
console.error('Certificate refresh failed:', error); console.error('Certificate refresh failed:', error);
} }
} }
// Refresh remote ingress data if on remoteingress view
if (currentView === 'remoteingress') {
try {
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
} catch (error) {
console.error('Remote ingress refresh failed:', error);
}
}
} catch (error) { } catch (error) {
console.error('Combined refresh failed:', error); console.error('Combined refresh failed:', error);
// If the error looks like an auth failure (invalid JWT), force re-login
const errMsg = String(error);
if (errMsg.includes('invalid') || errMsg.includes('unauthorized') || errMsg.includes('401')) {
await loginStatePart.dispatchAction(logoutAction, null);
window.location.reload();
}
} }
} }

View File

@@ -1,5 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js'; import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { appRouter } from '../router.js'; import { appRouter } from '../router.js';
import { import {
@@ -218,13 +219,27 @@ export class OpsDashboard extends DeesElement {
// Handle initial state - check if we have a stored session that's still valid // Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState(); const loginState = appstate.loginStatePart.getState();
if (loginState.identity?.jwt) { if (loginState.identity?.jwt) {
// Verify JWT hasn't expired
if (loginState.identity.expiresAt > Date.now()) { if (loginState.identity.expiresAt > Date.now()) {
// JWT still valid, restore logged-in state // Client-side expiry looks valid — verify with server (keypair may have changed)
this.loginState = loginState; try {
await simpleLogin.switchToSlottedContent(); const verifyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); interfaces.requests.IReq_VerifyIdentity
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); >('/typedrequest', 'verifyIdentity');
const response = await verifyRequest.fire({ identity: loginState.identity });
if (response.valid) {
// JWT confirmed valid by server
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
// Server rejected the JWT — clear state, show login
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} catch {
// Server unreachable or error — clear state, show login
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} else { } else {
// JWT expired, clear the stored state // JWT expired, clear the stored state
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);

View File

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

View File

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

View File

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