Compare commits

...

131 Commits

Author SHA1 Message Date
bd6130013c v11.2.1
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-08 18:58:29 +00:00
0f814bbcdd fix(deps): bump devDependency @git.zone/tstest to ^3.3.0 and dependency @push.rocks/smartproxy to ^25.9.2 2026-03-08 18:58:29 +00:00
8ec94b7dae v11.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-03-06 08:01:05 +00:00
d5dfe439c7 feat(apiclient): add typed, object-oriented API client documentation and interfaces; document builders, resource managers, and new programmatic endpoints 2026-03-06 08:01:05 +00:00
aaf3c9cb1c v11.1.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-03-06 07:52:10 +00:00
abde872ab2 feat(apiclient): add TypeScript API client (ts_apiclient) with resource managers and package exports 2026-03-06 07:52:10 +00:00
ca2d2b09ad v11.0.51
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 19:06:53 +00:00
fb7d4d988b fix(build): include HTML files in tsbundle output and bump tsbuild/tsbundle devDependencies 2026-03-05 19:06:53 +00:00
26e6eea5d5 v11.0.50
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 16:00:58 +00:00
2458dd08d8 fix(devDependencies): bump @git.zone/tsbuild to ^4.2.4 2026-03-05 16:00:58 +00:00
dee648b3bc v11.0.49
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:58:14 +00:00
f4ed32cee4 fix(dcrouter): no changes detected 2026-03-05 15:58:14 +00:00
e9c72952ab v11.0.48
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:57:20 +00:00
1bd485c43e fix(deps): bump @git.zone/tsbuild to ^4.2.3 2026-03-05 15:57:20 +00:00
421a0390ba v11.0.47
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:50:09 +00:00
c7f87a7c22 fix(dcrouter): no code changes; nothing to release 2026-03-05 15:50:09 +00:00
390d5c648f v11.0.46
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:38:01 +00:00
ec651c1cdb fix(none): no changes detected 2026-03-05 15:38:01 +00:00
6f82c393e7 v11.0.45
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 15:37:09 +00:00
afdb48367b fix(deps): bump @git.zone/tsbuild to ^4.2.2 2026-03-05 15:37:09 +00:00
53526ca3ba v11.0.44
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:29:31 +00:00
07e8f4489b fix(dev-deps): bump @git.zone/tsbuild devDependency to ^4.2.1 2026-03-05 15:29:31 +00:00
14101a09d3 v11.0.43
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:26:05 +00:00
5344d53806 fix(dcrouter): no changes detected; nothing to release 2026-03-05 15:26:05 +00:00
971535926c v11.0.42
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:24:56 +00:00
c13a4ae4be fix(dcrouter): empty commit — no changes 2026-03-05 15:24:55 +00:00
e7a03c48ae v11.0.41
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:23:36 +00:00
a682329a3f fix(deps): bump devDependency @git.zone/tsbuild to ^4.2.0 2026-03-05 15:23:36 +00:00
c4580f9874 v11.0.40
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:19:39 +00:00
b331065b8c fix(deps): bump @git.zone/tsbuild devDependency to ^4.1.26 2026-03-05 15:19:39 +00:00
4675ca3e89 v11.0.39
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:15:25 +00:00
70e2c8e17d fix(devDependencies): bump @git.zone/tsbuild devDependency to ^4.1.25 2026-03-05 15:15:25 +00:00
db53d87cc5 v11.0.38
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:11:21 +00:00
ff6244d3d1 fix(deps): bump @git.zone/tsbuild to ^4.1.24 2026-03-05 15:11:20 +00:00
f0aafe9027 v11.0.37
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:07:45 +00:00
487f2acac8 fix(dcrouter): bump patch version (no changes detected) 2026-03-05 15:07:45 +00:00
0a5e35c58e v11.0.36
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:05:33 +00:00
34c0cab5dc fix(repo): no changes detected; no release necessary 2026-03-05 15:05:33 +00:00
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
71 changed files with 4325 additions and 2246 deletions

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ dist_*/
**/.claude/settings.local.json
.nogit/data/
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,427 @@
# Changelog
## 2026-03-08 - 11.2.1 - fix(deps)
bump devDependency @git.zone/tstest to ^3.3.0 and dependency @push.rocks/smartproxy to ^25.9.2
- Bumped devDependency @git.zone/tstest: ^3.2.0 -> ^3.3.0
- Bumped dependency @push.rocks/smartproxy: ^25.9.1 -> ^25.9.2
- Current package version: 11.2.0 — recommended patch release to 11.2.1
## 2026-03-06 - 11.2.0 - feat(apiclient)
add typed, object-oriented API client documentation and interfaces; document builders, resource managers, and new programmatic endpoints
- Add new @serve.zone/dcrouter-apiclient documentation (ts_apiclient/readme.md) and publish ordering (ts_apiclient/tspublish.json).
- Document OO resource classes, fluent builders, auth modes, examples, and API surface for routes, certificates, apiTokens, remoteIngress, stats, config, logs, emails, and radius.
- Update main readme: add API Client section, list new client methods, add package entry for @serve.zone/dcrouter-apiclient, and add apiclient test coverage entry.
- Update interfaces readme: add Route Management and API Token Management request interfaces and email method changes (getAllEmails, getEmailDetail).
- API reference changes: consolidate email endpoints (getAllEmails/getEmailDetail), add route and api token management methods, rename getLogs to getRecentLogs and add getLogStream.
- Update web docs to include route & API token management pages and ops view (ops-view-routes)
## 2026-03-06 - 11.1.0 - feat(apiclient)
add TypeScript API client (ts_apiclient) with resource managers and package exports
- Add new ts_apiclient module providing DcRouterApiClient and resource managers: routes, certificates, api tokens, remote ingress, emails, stats, config, logs, and radius (with sub-managers).
- Add resource classes and builders (Route, RemoteIngress, ApiToken, Certificate, Email) and convenience manager APIs for common operations.
- Export apiclient in package.json (exports and files) and add ts_apiclient index and plugins wrapper for @api.global/typedrequest.
- Add comprehensive tests for the API client (test/test.apiclient.ts).
- Bump devDependencies: @git.zone/tsbuild -> ^4.3.0 and @types/node -> ^25.3.5
## 2026-03-05 - 11.0.51 - fix(build)
include HTML files in tsbundle output and bump tsbuild/tsbundle devDependencies
- Add includeFiles: ["./html/**/*.html"] to bundler config in npmextra.json so HTML assets are included in the bundle
- Bump devDependencies: @git.zone/tsbuild ^4.2.4 -> ^4.2.6, @git.zone/tsbundle ^2.9.0 -> ^2.9.1 (non-breaking tooling updates)
## 2026-03-05 - 11.0.50 - fix(devDependencies)
bump @git.zone/tsbuild to ^4.2.4
- updated devDependency @git.zone/tsbuild from ^4.2.3 to ^4.2.4
- no other package changes
## 2026-03-05 - 11.0.49 - fix(dcrouter)
no changes detected
- No files changed in this commit
- Working tree unchanged; no version bump required
## 2026-03-05 - 11.0.48 - fix(deps)
bump @git.zone/tsbuild to ^4.2.3
- package.json: updated devDependency @git.zone/tsbuild from ^4.2.2 to ^4.2.3
## 2026-03-05 - 11.0.47 - fix(dcrouter)
no code changes; nothing to release
- No files changed in this commit (git diff is empty)
- No version bump required
## 2026-03-05 - 11.0.46 - fix(none)
no changes detected
- Git diff reported no changes
- No files were modified; no version bump required
## 2026-03-05 - 11.0.45 - fix(deps)
bump @git.zone/tsbuild to ^4.2.2
- Updated @git.zone/tsbuild from ^4.2.1 to ^4.2.2 in package.json
## 2026-03-05 - 11.0.44 - fix(dev-deps)
bump @git.zone/tsbuild devDependency to ^4.2.1
- Updated package.json devDependency @git.zone/tsbuild from ^4.2.0 to ^4.2.1
- Non-breaking patch update for build tool dependency
## 2026-03-05 - 11.0.43 - fix(dcrouter)
no changes detected; nothing to release
- Git diff reported no changes
- No files were modified, so no version bump is recommended
## 2026-03-05 - 11.0.42 - fix(dcrouter)
empty commit — no changes
- No files were modified in this commit
- No version bump required
## 2026-03-05 - 11.0.41 - fix(deps)
bump devDependency @git.zone/tsbuild to ^4.2.0
- Updated @git.zone/tsbuild from ^4.1.26 to ^4.2.0
- Change made in package.json under devDependencies
- No source code changes — dev tooling dependency bump
## 2026-03-05 - 11.0.40 - fix(deps)
bump @git.zone/tsbuild devDependency to ^4.1.26
- Updated devDependency @git.zone/tsbuild: ^4.1.25 → ^4.1.26 in package.json
- Build tooling/dev dependency bump only; no runtime or API changes
## 2026-03-05 - 11.0.39 - fix(devDependencies)
bump @git.zone/tsbuild devDependency to ^4.1.25
- Updated devDependency @git.zone/tsbuild from ^4.1.24 to ^4.1.25 in package.json
- Only a devDependency was changed; no runtime dependencies or source files modified
- Current package version is 11.0.38; recommend a patch release
## 2026-03-05 - 11.0.38 - fix(deps)
bump @git.zone/tsbuild to ^4.1.24
- Updated @git.zone/tsbuild in devDependencies from ^4.1.23 to ^4.1.24
- Dev tooling dependency bump; no runtime or API changes expected
## 2026-03-05 - 11.0.37 - fix(dcrouter)
bump patch version (no changes detected)
- No files changed in the provided diff
- Current package version is 11.0.36 (package.json)
- Recommend a patch bump to record a new release if desired
## 2026-03-05 - 11.0.36 - fix(repo)
no changes detected; no release necessary
- Diff contains no changes
- No files were modified — skip version bump
## 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

View File

@@ -22,7 +22,8 @@
"to": "./dist_serve/bundle.js",
"outputMode": "bundle",
"bundler": "esbuild",
"production": true
"production": true,
"includeFiles": ["./html/**/*.html"]
}
]
},

View File

@@ -1,12 +1,13 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "9.3.0",
"version": "11.2.1",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
".": "./dist_ts/index.js",
"./interfaces": "./dist_ts_interfaces/index.js"
"./interfaces": "./dist_ts_interfaces/index.js",
"./apiclient": "./dist_ts_apiclient/index.js"
},
"author": "Task Venture Capital GmbH",
"license": "MIT",
@@ -19,21 +20,22 @@
"watch": "tswatch"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "^3.2.0",
"@types/node": "^25.3.0"
"@git.zone/tstest": "^3.3.0",
"@git.zone/tswatch": "^3.2.5",
"@types/node": "^25.3.5"
},
"dependencies": {
"@api.global/typedrequest": "^3.2.6",
"@api.global/typedrequest": "^3.3.0",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.4.0",
"@api.global/typedserver": "^8.4.2",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6",
"@push.rocks/lik": "^6.3.1",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3",
@@ -43,21 +45,21 @@
"@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.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/smartmta": "^5.3.0",
"@push.rocks/smartmta": "^5.3.1",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^25.8.5",
"@push.rocks/smartproxy": "^25.9.2",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.0.30",
"@push.rocks/smartstate": "^2.2.0",
"@push.rocks/smartunique": "^3.0.9",
"@serve.zone/catalog": "^2.5.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.3.0",
"@serve.zone/remoteingress": "^4.4.0",
"@tsclass/tsclass": "^9.3.0",
"lru-cache": "^11.2.6",
"uuid": "^13.0.0"
@@ -99,10 +101,12 @@
"files": [
"ts/**/*",
"ts_web/**/*",
"ts_apiclient/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"dist_ts_apiclient/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",

2821
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

110
readme.md
View File

@@ -26,6 +26,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [Storage & Caching](#storage--caching)
- [Security Features](#security-features)
- [OpsServer Dashboard](#opsserver-dashboard)
- [API Client](#api-client)
- [API Reference](#api-reference)
- [Sub-Modules](#sub-modules)
- [Testing](#testing)
@@ -90,6 +91,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Remote ingress management** with connection token generation and one-click copy
- **Read-only configuration display** — DcRouter is configured through code
### 🔧 Programmatic API Client
- **Object-oriented API** — resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) with instance methods
- **Builder pattern** — fluent `.setName().setMatch().save()` chains for creating routes, tokens, and edges
- **Auto-injected auth** — JWT identity and API tokens included automatically in every request
- **Dual auth modes** — login with credentials (JWT) or pass an API token for programmatic access
- **Full coverage** — wraps every OpsServer endpoint with typed request/response pairs
## Installation
```bash
@@ -1038,12 +1046,9 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'getCombinedMetrics' // All metrics in one call
// Email Operations
'getQueuedEmails' // Emails pending delivery
'getSentEmails' // Successfully delivered emails
'getFailedEmails' // Failed emails
'getAllEmails' // List all emails (queued/sent/failed)
'getEmailDetail' // Full detail for a specific email
'resendEmail' // Re-queue a failed email
'getBounceRecords' // Bounce records
'removeFromSuppressionList' // Unsuppress an address
// Certificates
'getCertificateOverview' // Domain-centric certificate status
@@ -1062,11 +1067,28 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'getRemoteIngressStatus' // Runtime status of all edges
'getRemoteIngressConnectionToken' // Generate a connection token for an edge
// Route Management (JWT or API token auth)
'getMergedRoutes' // List all routes (hardcoded + programmatic)
'createRoute' // Create a new programmatic route
'updateRoute' // Update a programmatic route
'deleteRoute' // Delete a programmatic route
'toggleRoute' // Enable/disable a programmatic route
'setRouteOverride' // Override a hardcoded route
'removeRouteOverride' // Remove a hardcoded route override
// API Token Management (admin JWT only)
'createApiToken' // Create API token → returns raw value once
'listApiTokens' // List all tokens (without secrets)
'revokeApiToken' // Delete an API token
'rollApiToken' // Regenerate token secret
'toggleApiToken' // Enable/disable a token
// Configuration (read-only)
'getConfiguration' // Current system config
// Logs
'getLogs' // Retrieve system logs
'getRecentLogs' // Retrieve system logs with filtering
'getLogStream' // Stream live logs
// RADIUS
'getRadiusSessions' // Active RADIUS sessions
@@ -1080,6 +1102,77 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'testVlanAssignment' // Test what VLAN a MAC gets
```
## API Client
DcRouter ships with a typed, object-oriented API client for programmatic management of a running instance. Install it separately or import from the main package:
```bash
pnpm add @serve.zone/dcrouter-apiclient
# or import from the main package:
# import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
```
### Quick Example
```typescript
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
await client.login('admin', 'password');
// OO resource instances with methods
const { routes } = await client.routes.list();
await routes[0].toggle(false);
// Builder pattern for creation
const newRoute = await client.routes.build()
.setName('api-gateway')
.setMatch({ ports: 443, domains: ['api.example.com'] })
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
.setTls({ mode: 'terminate', certificate: 'auto' })
.save();
// Manage certificates
const { certificates, summary } = await client.certificates.list();
await certificates[0].reprovision();
// Create API tokens with builder
const token = await client.apiTokens.build()
.setName('ci-token')
.setScopes(['routes:read', 'routes:write'])
.setExpiresInDays(90)
.save();
console.log(token.tokenValue); // only available at creation
// Remote ingress edges
const edge = await client.remoteIngress.build()
.setName('edge-nyc-01')
.setListenPorts([80, 443])
.save();
const connToken = await edge.getConnectionToken();
// Read-only managers
const health = await client.stats.getHealth();
const config = await client.config.get();
const { logs } = await client.logs.getRecent({ level: 'error', limit: 50 });
```
### Resource Managers
| Manager | Operations |
|---------|-----------|
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
| `client.config` | `get(section?)` |
| `client.logs` | `getRecent()`, `getStream()` |
| `client.emails` | `list()` → Email: `getDetail()`, `resend()` |
| `client.radius` | `.clients`, `.vlans`, `.sessions` sub-managers + `getStatistics()`, `getAccountingSummary()` |
See the [full API client documentation](./ts_apiclient/readme.md) for detailed usage of every manager, builder, and resource class.
## API Reference
### DcRouter Class
@@ -1144,12 +1237,14 @@ DcRouter is published as a monorepo with separately-installable interface and we
|---------|-------------|---------|
| [`@serve.zone/dcrouter`](https://www.npmjs.com/package/@serve.zone/dcrouter) | Main package — the full router | `pnpm add @serve.zone/dcrouter` |
| [`@serve.zone/dcrouter-interfaces`](https://www.npmjs.com/package/@serve.zone/dcrouter-interfaces) | TypedRequest interfaces for the OpsServer API | `pnpm add @serve.zone/dcrouter-interfaces` |
| [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) | OO API client with builder pattern | `pnpm add @serve.zone/dcrouter-apiclient` |
| [`@serve.zone/dcrouter-web`](https://www.npmjs.com/package/@serve.zone/dcrouter-web) | Web dashboard components | `pnpm add @serve.zone/dcrouter-web` |
You can also import interfaces directly from the main package:
You can also import directly from the main package:
```typescript
import { data, requests } from '@serve.zone/dcrouter/interfaces';
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
```
## Testing
@@ -1171,6 +1266,7 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
| Test File | Area | Tests |
|-----------|------|-------|
| `test.apiclient.ts` | API client instantiation, builders, resource hydration, exports | 18 |
| `test.contentscanner.ts` | Content scanning (spam, phishing, malware, attachments) | 13 |
| `test.dcrouter.email.ts` | Email config, domain and route setup | 4 |
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |

376
test/test.apiclient.ts Normal file
View File

@@ -0,0 +1,376 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import {
DcRouterApiClient,
Route,
RouteBuilder,
RouteManager,
Certificate,
CertificateManager,
ApiToken,
ApiTokenBuilder,
ApiTokenManager,
RemoteIngress,
RemoteIngressBuilder,
RemoteIngressManager,
Email,
EmailManager,
StatsManager,
ConfigManager,
LogManager,
RadiusManager,
RadiusClientManager,
RadiusVlanManager,
RadiusSessionManager,
} from '../ts_apiclient/index.js';
// =============================================================================
// Instantiation & Structure
// =============================================================================
tap.test('DcRouterApiClient - should instantiate with baseUrl', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
expect(client).toBeTruthy();
expect(client.baseUrl).toEqual('https://localhost:3000');
expect(client.identity).toBeUndefined();
});
tap.test('DcRouterApiClient - should strip trailing slashes from baseUrl', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000///' });
expect(client.baseUrl).toEqual('https://localhost:3000');
});
tap.test('DcRouterApiClient - should accept optional apiToken', async () => {
const client = new DcRouterApiClient({
baseUrl: 'https://localhost:3000',
apiToken: 'dcr_test_token',
});
expect(client.apiToken).toEqual('dcr_test_token');
});
tap.test('DcRouterApiClient - should have all resource managers', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
expect(client.routes).toBeInstanceOf(RouteManager);
expect(client.certificates).toBeInstanceOf(CertificateManager);
expect(client.apiTokens).toBeInstanceOf(ApiTokenManager);
expect(client.remoteIngress).toBeInstanceOf(RemoteIngressManager);
expect(client.stats).toBeInstanceOf(StatsManager);
expect(client.config).toBeInstanceOf(ConfigManager);
expect(client.logs).toBeInstanceOf(LogManager);
expect(client.emails).toBeInstanceOf(EmailManager);
expect(client.radius).toBeInstanceOf(RadiusManager);
});
// =============================================================================
// buildRequestPayload
// =============================================================================
tap.test('DcRouterApiClient - buildRequestPayload includes identity when set', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const identity = {
jwt: 'test-jwt',
userId: 'user1',
name: 'Admin',
expiresAt: Date.now() + 3600000,
};
client.identity = identity;
const payload = client.buildRequestPayload({ extra: 'data' });
expect(payload.identity).toEqual(identity);
expect(payload.extra).toEqual('data');
});
tap.test('DcRouterApiClient - buildRequestPayload includes apiToken when set', async () => {
const client = new DcRouterApiClient({
baseUrl: 'https://localhost:3000',
apiToken: 'dcr_abc123',
});
const payload = client.buildRequestPayload();
expect(payload.apiToken).toEqual('dcr_abc123');
});
tap.test('DcRouterApiClient - buildRequestPayload with both identity and apiToken', async () => {
const client = new DcRouterApiClient({
baseUrl: 'https://localhost:3000',
apiToken: 'dcr_abc123',
});
client.identity = {
jwt: 'test-jwt',
userId: 'user1',
name: 'Admin',
expiresAt: Date.now() + 3600000,
};
const payload = client.buildRequestPayload({ foo: 'bar' });
expect(payload.identity).toBeTruthy();
expect(payload.apiToken).toEqual('dcr_abc123');
expect(payload.foo).toEqual('bar');
});
// =============================================================================
// Route Builder
// =============================================================================
tap.test('RouteBuilder - should support fluent builder pattern', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const builder = client.routes.build();
expect(builder).toBeInstanceOf(RouteBuilder);
// Fluent methods return `this` (same reference)
const result = builder
.setName('test-route')
.setMatch({ ports: 443, domains: 'example.com' })
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
.setEnabled(true);
expect(result === builder).toBeTrue();
});
// =============================================================================
// ApiToken Builder
// =============================================================================
tap.test('ApiTokenBuilder - should support fluent builder pattern', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const builder = client.apiTokens.build();
expect(builder).toBeInstanceOf(ApiTokenBuilder);
const result = builder
.setName('ci-token')
.setScopes(['routes:read', 'routes:write'])
.addScope('config:read')
.setExpiresInDays(30);
expect(result === builder).toBeTrue();
});
// =============================================================================
// RemoteIngress Builder
// =============================================================================
tap.test('RemoteIngressBuilder - should support fluent builder pattern', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const builder = client.remoteIngress.build();
expect(builder).toBeInstanceOf(RemoteIngressBuilder);
const result = builder
.setName('edge-1')
.setListenPorts([80, 443])
.setAutoDerivePorts(true)
.setTags(['production']);
expect(result === builder).toBeTrue();
});
// =============================================================================
// Route resource class
// =============================================================================
tap.test('Route - should hydrate from IMergedRoute data', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const route = new Route(client, {
route: {
name: 'test-route',
match: { ports: 443, domains: 'example.com' },
action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }] },
},
source: 'programmatic',
enabled: true,
overridden: false,
storedRouteId: 'route-123',
createdAt: 1000,
updatedAt: 2000,
});
expect(route.name).toEqual('test-route');
expect(route.source).toEqual('programmatic');
expect(route.enabled).toEqual(true);
expect(route.overridden).toEqual(false);
expect(route.storedRouteId).toEqual('route-123');
expect(route.routeConfig.match.ports).toEqual(443);
});
tap.test('Route - should throw on update/delete/toggle for hardcoded routes', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const route = new Route(client, {
route: {
name: 'hardcoded-route',
match: { ports: 80 },
action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
},
source: 'hardcoded',
enabled: true,
overridden: false,
// No storedRouteId for hardcoded routes
});
let updateError: Error | undefined;
try {
await route.update({ name: 'new-name' });
} catch (e) {
updateError = e as Error;
}
expect(updateError).toBeTruthy();
expect(updateError!.message).toInclude('hardcoded');
let deleteError: Error | undefined;
try {
await route.delete();
} catch (e) {
deleteError = e as Error;
}
expect(deleteError).toBeTruthy();
let toggleError: Error | undefined;
try {
await route.toggle(false);
} catch (e) {
toggleError = e as Error;
}
expect(toggleError).toBeTruthy();
});
// =============================================================================
// Certificate resource class
// =============================================================================
tap.test('Certificate - should hydrate from ICertificateInfo data', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const cert = new Certificate(client, {
domain: 'example.com',
routeNames: ['main-route'],
status: 'valid',
source: 'acme',
tlsMode: 'terminate',
expiryDate: '2027-01-01T00:00:00Z',
issuer: "Let's Encrypt",
canReprovision: true,
});
expect(cert.domain).toEqual('example.com');
expect(cert.status).toEqual('valid');
expect(cert.source).toEqual('acme');
expect(cert.canReprovision).toEqual(true);
expect(cert.routeNames.length).toEqual(1);
});
// =============================================================================
// ApiToken resource class
// =============================================================================
tap.test('ApiToken - should hydrate from IApiTokenInfo data', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const token = new ApiToken(
client,
{
id: 'token-1',
name: 'ci-token',
scopes: ['routes:read', 'routes:write'],
createdAt: Date.now(),
expiresAt: null,
lastUsedAt: null,
enabled: true,
},
'dcr_secret_value',
);
expect(token.id).toEqual('token-1');
expect(token.name).toEqual('ci-token');
expect(token.scopes.length).toEqual(2);
expect(token.enabled).toEqual(true);
expect(token.tokenValue).toEqual('dcr_secret_value');
});
// =============================================================================
// RemoteIngress resource class
// =============================================================================
tap.test('RemoteIngress - should hydrate from IRemoteIngress data', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const edge = new RemoteIngress(client, {
id: 'edge-1',
name: 'test-edge',
secret: 'secret123',
listenPorts: [80, 443],
enabled: true,
autoDerivePorts: true,
tags: ['prod'],
createdAt: 1000,
updatedAt: 2000,
effectiveListenPorts: [80, 443, 8080],
manualPorts: [80, 443],
derivedPorts: [8080],
});
expect(edge.id).toEqual('edge-1');
expect(edge.name).toEqual('test-edge');
expect(edge.listenPorts.length).toEqual(2);
expect(edge.effectiveListenPorts!.length).toEqual(3);
expect(edge.autoDerivePorts).toEqual(true);
});
// =============================================================================
// Email resource class
// =============================================================================
tap.test('Email - should hydrate from IEmail data', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
const email = new Email(client, {
id: 'email-1',
direction: 'inbound',
status: 'delivered',
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test email',
timestamp: '2026-03-06T00:00:00Z',
messageId: '<msg-1@example.com>',
size: '1234',
});
expect(email.id).toEqual('email-1');
expect(email.direction).toEqual('inbound');
expect(email.status).toEqual('delivered');
expect(email.from).toEqual('sender@example.com');
expect(email.subject).toEqual('Test email');
});
// =============================================================================
// RadiusManager structure
// =============================================================================
tap.test('RadiusManager - should have sub-managers', async () => {
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
expect(client.radius.clients).toBeInstanceOf(RadiusClientManager);
expect(client.radius.vlans).toBeInstanceOf(RadiusVlanManager);
expect(client.radius.sessions).toBeInstanceOf(RadiusSessionManager);
});
// =============================================================================
// Exports verification
// =============================================================================
tap.test('Exports - all expected classes should be importable', async () => {
expect(DcRouterApiClient).toBeTruthy();
expect(Route).toBeTruthy();
expect(RouteBuilder).toBeTruthy();
expect(RouteManager).toBeTruthy();
expect(Certificate).toBeTruthy();
expect(CertificateManager).toBeTruthy();
expect(ApiToken).toBeTruthy();
expect(ApiTokenBuilder).toBeTruthy();
expect(ApiTokenManager).toBeTruthy();
expect(RemoteIngress).toBeTruthy();
expect(RemoteIngressBuilder).toBeTruthy();
expect(RemoteIngressManager).toBeTruthy();
expect(Email).toBeTruthy();
expect(EmailManager).toBeTruthy();
expect(StatsManager).toBeTruthy();
expect(ConfigManager).toBeTruthy();
expect(LogManager).toBeTruthy();
expect(RadiusManager).toBeTruthy();
expect(RadiusClientManager).toBeTruthy();
expect(RadiusVlanManager).toBeTruthy();
expect(RadiusSessionManager).toBeTruthy();
});
export default tap.start();

View File

@@ -4,27 +4,44 @@ import { TypedRequest } from '@api.global/typedrequest';
import * as interfaces from '../ts_interfaces/index.js';
let testDcRouter: DcRouter;
let adminIdentity: interfaces.data.IIdentity;
tap.test('should start DCRouter with OpsServer', async () => {
testDcRouter = new DcRouter({
// Minimal config for testing
cacheConfig: { enabled: false },
});
await testDcRouter.start();
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 () => {
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
'http://localhost:3000/typedrequest',
'getHealthStatus'
);
const response = await healthRequest.fire({
detailed: false
identity: adminIdentity,
detailed: false,
});
expect(response).toHaveProperty('health');
expect(response.health.healthy).toBeTrue();
expect(response.health.services).toHaveProperty('OpsServer');
@@ -35,11 +52,12 @@ tap.test('should respond to server statistics request', async () => {
'http://localhost:3000/typedrequest',
'getServerStatistics'
);
const response = await statsRequest.fire({
includeHistory: false
identity: adminIdentity,
includeHistory: false,
});
expect(response).toHaveProperty('stats');
expect(response.stats).toHaveProperty('uptime');
expect(response.stats).toHaveProperty('cpuUsage');
@@ -51,9 +69,11 @@ tap.test('should respond to configuration request', async () => {
'http://localhost:3000/typedrequest',
'getConfiguration'
);
const response = await configRequest.fire({});
const response = await configRequest.fire({
identity: adminIdentity,
});
expect(response).toHaveProperty('config');
expect(response.config).toHaveProperty('system');
expect(response.config).toHaveProperty('smartProxy');
@@ -70,19 +90,34 @@ tap.test('should handle log retrieval request', async () => {
'http://localhost:3000/typedrequest',
'getRecentLogs'
);
const response = await logsRequest.fire({
limit: 10
identity: adminIdentity,
limit: 10,
});
expect(response).toHaveProperty('logs');
expect(response).toHaveProperty('total');
expect(response).toHaveProperty('hasMore');
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 () => {
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>(
'http://localhost:3000/typedrequest',
'getHealthStatus'
);
// No identity provided
const response = await healthRequest.fire({});
expect(response).toHaveProperty('health');
expect(response.health.healthy).toBeTrue();
console.log('Public endpoint accessible without auth');
try {
// No identity provided — should be rejected
await healthRequest.fire({} as any);
expect(true).toBeFalse(); // Should not reach here
} catch (error) {
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>(
'http://localhost:3000/typedrequest',
'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.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('radius');
expect(response.config).toHaveProperty('remoteIngress');
console.log('Configuration read successfully');
console.log('Authenticated access to config successful');
});
tap.test('should stop DCRouter', async () => {

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '9.3.0',
version: '11.2.1',
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 { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -221,7 +222,8 @@ export class DcRouter {
public detectedPublicIp: string | null = null;
// 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 dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
@@ -900,7 +902,8 @@ export class DcRouter {
}
this.dnsBatchTimer = null;
this.dnsBatchCount = 0;
this.dnsLogWindow = [];
this.dnsLogWindowSecond = 0;
this.dnsLogWindowCount = 0;
}
await this.opsServer.stop();
@@ -956,6 +959,7 @@ export class DcRouter {
// Stop cache database after other services (they may need it during shutdown)
if (this.cacheDb) {
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
CacheDb.resetInstance();
}
// Clear backoff cache in cert scheduler
@@ -979,6 +983,11 @@ export class DcRouter {
this.apiTokenManager = undefined;
this.certificateStatusMap.clear();
// Reset security singletons to allow GC
SecurityLogger.resetInstance();
ContentScanner.resetInstance();
IPReputationChecker.resetInstance();
logger.log('info', 'All DcRouter services stopped');
} catch (error) {
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
@@ -1305,11 +1314,14 @@ export class DcRouter {
}
// Adaptive logging: individual logs up to 2/sec, then batch
const now = Date.now();
this.dnsLogWindow = this.dnsLogWindow.filter(t => now - t < 1000);
const nowSec = Math.floor(Date.now() / 1000);
if (nowSec !== this.dnsLogWindowSecond) {
this.dnsLogWindowSecond = nowSec;
this.dnsLogWindowCount = 0;
}
if (this.dnsLogWindow.length < 2) {
this.dnsLogWindow.push(now);
if (this.dnsLogWindowCount < 2) {
this.dnsLogWindowCount++;
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' });
} else {
@@ -1363,15 +1375,25 @@ export class DcRouter {
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');
try {
// Use the built-in socket handler from smartdns
// This handles HTTP/2, DoH protocol, etc.
await (this.dnsServer as any).handleHttpsSocket(socket);
} catch (error) {
logger.log('error', `DNS socket handler error: ${error.message}`);
socket.destroy();
if (!socket.destroyed) {
socket.destroy();
}
}
};
}

View File

@@ -122,6 +122,24 @@ export class ApiTokenManager {
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.
*/

View File

@@ -35,7 +35,9 @@ export class MetricsManager {
queryTypes: {} as Record<string, number>,
topDomains: new Map<string, number>(),
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
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.queryTypes = {};
this.dnsMetrics.topDomains.clear();
this.dnsMetrics.queryTimestamps = [];
this.dnsMetrics.queryRing.fill(0);
this.dnsMetrics.queryRingLastSecond = 0;
this.dnsMetrics.responseTimes = [];
this.dnsMetrics.recentQueries = [];
this.dnsMetrics.lastResetDate = currentDate;
}
if (currentDate !== this.securityMetrics.lastResetDate) {
this.securityMetrics.blockedIPs = 0;
this.securityMetrics.authFailures = 0;
@@ -141,16 +144,16 @@ export class MetricsManager {
const smartMetricsData = await this.smartMetrics.getMetrics();
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
return {
uptime: process.uptime(),
startTime: Date.now() - (process.uptime() * 1000),
memoryUsage: {
heapUsed: process.memoryUsage().heapUsed,
heapTotal: process.memoryUsage().heapTotal,
external: process.memoryUsage().external,
rss: process.memoryUsage().rss,
// Add SmartMetrics memory data
heapUsed,
heapTotal,
external,
rss,
maxMemoryMB: this.smartMetrics.maxMemoryMB,
actualUsageBytes: smartMetricsData.memoryUsageBytes,
actualUsagePercentage: smartMetricsData.memoryPercentage,
@@ -219,11 +222,8 @@ export class MetricsManager {
.slice(0, 10)
.map(([domain, count]) => ({ domain, count }));
// Calculate queries per second from recent timestamps
const now = Date.now();
const oneMinuteAgo = now - 60000;
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
const queriesPerSecond = recentQueries.length / 60;
// Calculate queries per second from ring buffer (sum last 60 seconds)
const queriesPerSecond = this.getQueryRingSum(60) / 60;
// Calculate average response time
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
@@ -427,12 +427,8 @@ export class MetricsManager {
this.dnsMetrics.cacheMisses++;
}
// Track query timestamp
this.dnsMetrics.queryTimestamps.push(Date.now());
// Keep only timestamps from last 5 minutes
const fiveMinutesAgo = Date.now() - 300000;
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
// Increment per-second query counter in ring buffer
this.incrementQueryRing();
// Track response time if provided
if (responseTimeMs) {
@@ -604,7 +600,7 @@ export class MetricsManager {
requestsPerSecond,
requestsTotal,
};
}, 200); // Use 200ms cache for more frequent updates
}, 1000); // 1s cache — matches typical dashboard poll interval
}
// --- Time-series helpers ---
@@ -633,6 +629,63 @@ export class MetricsManager {
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 {
const cutoff = Date.now() - 86400000; // 24h
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 paths from '../paths.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 {
public dcRouterRef: DcRouter;
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();
// 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
public adminHandler: handlers.AdminHandler;
private configHandler: handlers.ConfigHandler;
@@ -25,7 +31,7 @@ export class OpsServer {
constructor(dcRouterRefArg: DcRouter) {
this.dcRouterRef = dcRouterRefArg;
// Add our typedrouter to the dcRouter's main typedrouter
this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter);
}
@@ -51,10 +57,25 @@ export class OpsServer {
* Set up all TypedRequest handlers
*/
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);
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.logsHandler = new handlers.LogsHandler(this);
this.securityHandler = new handlers.SecurityHandler(this);

View File

@@ -3,34 +3,20 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
export class ApiTokenHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
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 {
// 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
this.typedrouter.addTypedHandler(
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
'createApiToken',
async (dataArg) => {
const userId = await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) {
return { success: false, message: 'Token management not initialized' };
@@ -39,7 +25,7 @@ export class ApiTokenHandler {
dataArg.name,
dataArg.scopes,
dataArg.expiresInDays ?? null,
userId,
dataArg.identity.userId,
);
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
},
@@ -47,11 +33,10 @@ export class ApiTokenHandler {
);
// List API tokens
this.typedrouter.addTypedHandler(
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
'listApiTokens',
async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) {
return { tokens: [] };
@@ -62,11 +47,10 @@ export class ApiTokenHandler {
);
// Revoke API token
this.typedrouter.addTypedHandler(
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
'revokeApiToken',
async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) {
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
this.typedrouter.addTypedHandler(
router.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
'toggleApiToken',
async (dataArg) => {
await this.requireAdmin(dataArg.identity);
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
if (!manager) {
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';
export class CertificateHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
const viewRouter = this.opsServerRef.viewRouter;
const adminRouter = this.opsServerRef.adminRouter;
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
// Get Certificate Overview
this.typedrouter.addTypedHandler(
viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
'getCertificateOverview',
async (dataArg) => {
@@ -23,8 +25,10 @@ export class CertificateHandler {
)
);
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
// Legacy route-based reprovision (backward compat)
this.typedrouter.addTypedHandler(
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
'reprovisionCertificate',
async (dataArg) => {
@@ -34,7 +38,7 @@ export class CertificateHandler {
);
// Domain-based reprovision (preferred)
this.typedrouter.addTypedHandler(
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
'reprovisionCertificateDomain',
async (dataArg) => {
@@ -44,7 +48,7 @@ export class CertificateHandler {
);
// Delete certificate
this.typedrouter.addTypedHandler(
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
'deleteCertificate',
async (dataArg) => {
@@ -54,7 +58,7 @@ export class CertificateHandler {
);
// Export certificate
this.typedrouter.addTypedHandler(
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
'exportCertificate',
async (dataArg) => {
@@ -64,7 +68,7 @@ export class CertificateHandler {
);
// Import certificate
this.typedrouter.addTypedHandler(
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
'importCertificate',
async (dataArg) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,6 +65,8 @@ export class IPReputationChecker {
private reputationCache: LRUCache<string, IReputationResult>;
private options: Required<IIPReputationOptions>;
private storageManager?: any; // StorageManager instance
private saveCacheTimer: ReturnType<typeof setTimeout> | null = null;
private static readonly SAVE_CACHE_DEBOUNCE_MS = 30_000;
// Default DNSBL servers
private static readonly DEFAULT_DNSBL_SERVERS = [
@@ -143,7 +145,20 @@ export class IPReputationChecker {
}
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
* @param ip IP address to check
@@ -213,12 +228,9 @@ export class IPReputationChecker {
// Update cache with result
this.reputationCache.set(ip, result);
// Save cache if enabled
// Schedule debounced cache save if enabled
if (this.options.enableLocalCache) {
// Fire and forget the save operation
this.saveCache().catch(error => {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
});
this.debouncedSaveCache();
}
// 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
*/

View File

@@ -83,7 +83,14 @@ export class SecurityLogger {
}
return SecurityLogger.instance;
}
/**
* Reset the singleton instance (for shutdown/testing)
*/
public static resetInstance(): void {
SecurityLogger.instance = undefined;
}
/**
* Log a security event
* @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
.slice()
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit);
}
@@ -242,58 +250,46 @@ export class SecurityLogger {
topIPs: Array<{ ip: string; count: number }>;
topDomains: Array<{ domain: string; count: number }>;
} {
// Filter by time window if provided
let events = this.securityEvents;
if (timeWindow) {
const cutoff = Date.now() - timeWindow;
events = events.filter(e => e.timestamp >= cutoff);
const cutoff = timeWindow ? Date.now() - timeWindow : 0;
// Initialize counters
const byLevel = {} as Record<SecurityLogLevel, number>;
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>();
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) {
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) {
domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
}
});
}
// Sort and limit top entries
const topIPs = Array.from(ipCounts.entries())
.map(([ip, count]) => ({ ip, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
const topDomains = Array.from(domainCounts.entries())
.map(([domain, count]) => ({ domain, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
return {
total: events.length,
byLevel,
byType,
topIPs,
topDomains
};
return { total, 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
*/
export class StorageManager {
private static readonly MAX_MEMORY_ENTRIES = 10_000;
private backend: StorageBackend;
private memoryStore: Map<string, string> = new Map();
private config: IStorageConfig;
@@ -227,6 +228,11 @@ export class StorageManager {
case 'memory': {
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;
}

View File

@@ -0,0 +1,157 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class ApiToken {
private clientRef: DcRouterApiClient;
// Data from IApiTokenInfo
public id: string;
public name: string;
public scopes: interfaces.data.TApiTokenScope[];
public createdAt: number;
public expiresAt: number | null;
public lastUsedAt: number | null;
public enabled: boolean;
/** Only set on creation or roll. Not persisted on server side. */
public tokenValue?: string;
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IApiTokenInfo, tokenValue?: string) {
this.clientRef = clientRef;
this.id = data.id;
this.name = data.name;
this.scopes = data.scopes;
this.createdAt = data.createdAt;
this.expiresAt = data.expiresAt;
this.lastUsedAt = data.lastUsedAt;
this.enabled = data.enabled;
this.tokenValue = tokenValue;
}
public async revoke(): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_RevokeApiToken>(
'revokeApiToken',
this.clientRef.buildRequestPayload({ id: this.id }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to revoke token');
}
}
public async roll(): Promise<string> {
const response = await this.clientRef.request<interfaces.requests.IReq_RollApiToken>(
'rollApiToken',
this.clientRef.buildRequestPayload({ id: this.id }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to roll token');
}
this.tokenValue = response.tokenValue;
return response.tokenValue!;
}
public async toggle(enabled: boolean): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleApiToken>(
'toggleApiToken',
this.clientRef.buildRequestPayload({ id: this.id, enabled }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to toggle token');
}
this.enabled = enabled;
}
}
export class ApiTokenBuilder {
private clientRef: DcRouterApiClient;
private tokenName: string = '';
private tokenScopes: interfaces.data.TApiTokenScope[] = [];
private tokenExpiresInDays?: number | null;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public setName(name: string): this {
this.tokenName = name;
return this;
}
public setScopes(scopes: interfaces.data.TApiTokenScope[]): this {
this.tokenScopes = scopes;
return this;
}
public addScope(scope: interfaces.data.TApiTokenScope): this {
if (!this.tokenScopes.includes(scope)) {
this.tokenScopes.push(scope);
}
return this;
}
public setExpiresInDays(days: number | null): this {
this.tokenExpiresInDays = days;
return this;
}
public async save(): Promise<ApiToken> {
const response = await this.clientRef.request<interfaces.requests.IReq_CreateApiToken>(
'createApiToken',
this.clientRef.buildRequestPayload({
name: this.tokenName,
scopes: this.tokenScopes,
expiresInDays: this.tokenExpiresInDays,
}) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to create API token');
}
return new ApiToken(
this.clientRef,
{
id: response.tokenId!,
name: this.tokenName,
scopes: this.tokenScopes,
createdAt: Date.now(),
expiresAt: this.tokenExpiresInDays
? Date.now() + this.tokenExpiresInDays * 24 * 60 * 60 * 1000
: null,
lastUsedAt: null,
enabled: true,
},
response.tokenValue,
);
}
}
export class ApiTokenManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<ApiToken[]> {
const response = await this.clientRef.request<interfaces.requests.IReq_ListApiTokens>(
'listApiTokens',
this.clientRef.buildRequestPayload() as any,
);
return response.tokens.map((t) => new ApiToken(this.clientRef, t));
}
public async create(options: {
name: string;
scopes: interfaces.data.TApiTokenScope[];
expiresInDays?: number | null;
}): Promise<ApiToken> {
return this.build()
.setName(options.name)
.setScopes(options.scopes)
.setExpiresInDays(options.expiresInDays ?? null)
.save();
}
public build(): ApiTokenBuilder {
return new ApiTokenBuilder(this.clientRef);
}
}

View File

@@ -0,0 +1,123 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class Certificate {
private clientRef: DcRouterApiClient;
// Data from ICertificateInfo
public domain: string;
public routeNames: string[];
public status: interfaces.requests.TCertificateStatus;
public source: interfaces.requests.TCertificateSource;
public tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
public expiryDate?: string;
public issuer?: string;
public issuedAt?: string;
public error?: string;
public canReprovision: boolean;
public backoffInfo?: {
failures: number;
retryAfter?: string;
lastError?: string;
};
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.ICertificateInfo) {
this.clientRef = clientRef;
this.domain = data.domain;
this.routeNames = data.routeNames;
this.status = data.status;
this.source = data.source;
this.tlsMode = data.tlsMode;
this.expiryDate = data.expiryDate;
this.issuer = data.issuer;
this.issuedAt = data.issuedAt;
this.error = data.error;
this.canReprovision = data.canReprovision;
this.backoffInfo = data.backoffInfo;
}
public async reprovision(): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_ReprovisionCertificateDomain>(
'reprovisionCertificateDomain',
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to reprovision certificate');
}
}
public async delete(): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteCertificate>(
'deleteCertificate',
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to delete certificate');
}
}
public async export(): Promise<{
id: string;
domainName: string;
created: number;
validUntil: number;
privateKey: string;
publicKey: string;
csr: string;
} | undefined> {
const response = await this.clientRef.request<interfaces.requests.IReq_ExportCertificate>(
'exportCertificate',
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to export certificate');
}
return response.cert;
}
}
export interface ICertificateSummary {
total: number;
valid: number;
expiring: number;
expired: number;
failed: number;
unknown: number;
}
export class CertificateManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<{ certificates: Certificate[]; summary: ICertificateSummary }> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetCertificateOverview>(
'getCertificateOverview',
this.clientRef.buildRequestPayload() as any,
);
return {
certificates: response.certificates.map((c) => new Certificate(this.clientRef, c)),
summary: response.summary,
};
}
public async import(cert: {
id: string;
domainName: string;
created: number;
validUntil: number;
privateKey: string;
publicKey: string;
csr: string;
}): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_ImportCertificate>(
'importCertificate',
this.clientRef.buildRequestPayload({ cert }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to import certificate');
}
}
}

View File

@@ -0,0 +1,17 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class ConfigManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async get(section?: string): Promise<interfaces.requests.IReq_GetConfiguration['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetConfiguration>(
'getConfiguration',
this.clientRef.buildRequestPayload({ section }) as any,
);
}
}

View File

@@ -0,0 +1,112 @@
import * as plugins from './plugins.js';
import * as interfaces from '../ts_interfaces/index.js';
import { RouteManager } from './classes.route.js';
import { CertificateManager } from './classes.certificate.js';
import { ApiTokenManager } from './classes.apitoken.js';
import { RemoteIngressManager } from './classes.remoteingress.js';
import { StatsManager } from './classes.stats.js';
import { ConfigManager } from './classes.config.js';
import { LogManager } from './classes.logs.js';
import { EmailManager } from './classes.email.js';
import { RadiusManager } from './classes.radius.js';
export interface IDcRouterApiClientOptions {
baseUrl: string;
apiToken?: string;
}
export class DcRouterApiClient {
public baseUrl: string;
public apiToken?: string;
public identity?: interfaces.data.IIdentity;
// Resource managers
public routes: RouteManager;
public certificates: CertificateManager;
public apiTokens: ApiTokenManager;
public remoteIngress: RemoteIngressManager;
public stats: StatsManager;
public config: ConfigManager;
public logs: LogManager;
public emails: EmailManager;
public radius: RadiusManager;
constructor(options: IDcRouterApiClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
this.apiToken = options.apiToken;
this.routes = new RouteManager(this);
this.certificates = new CertificateManager(this);
this.apiTokens = new ApiTokenManager(this);
this.remoteIngress = new RemoteIngressManager(this);
this.stats = new StatsManager(this);
this.config = new ConfigManager(this);
this.logs = new LogManager(this);
this.emails = new EmailManager(this);
this.radius = new RadiusManager(this);
}
// =====================
// Auth
// =====================
public async login(username: string, password: string): Promise<interfaces.data.IIdentity> {
const response = await this.request<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
{ username, password },
);
if (response.identity) {
this.identity = response.identity;
}
return response.identity!;
}
public async logout(): Promise<void> {
await this.request<interfaces.requests.IReq_AdminLogout>(
'adminLogout',
{ identity: this.identity! },
);
this.identity = undefined;
}
public async verifyIdentity(): Promise<{ valid: boolean; identity?: interfaces.data.IIdentity }> {
const response = await this.request<interfaces.requests.IReq_VerifyIdentity>(
'verifyIdentity',
{ identity: this.identity! },
);
if (response.identity) {
this.identity = response.identity;
}
return response;
}
// =====================
// Internal request helper
// =====================
public async request<T extends plugins.typedrequestInterfaces.ITypedRequest>(
method: string,
requestData: T['request'],
): Promise<T['response']> {
const typedRequest = new plugins.typedrequest.TypedRequest<T>(
`${this.baseUrl}/typedrequest`,
method,
);
return typedRequest.fire(requestData);
}
/**
* Build a request payload with identity and optional API token auto-injected.
*/
public buildRequestPayload(extra: Record<string, any> = {}): Record<string, any> {
const payload: Record<string, any> = { ...extra };
if (this.identity) {
payload.identity = this.identity;
}
if (this.apiToken) {
payload.apiToken = this.apiToken;
}
return payload;
}
}

View File

@@ -0,0 +1,77 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class Email {
private clientRef: DcRouterApiClient;
// Data from IEmail
public id: string;
public direction: interfaces.requests.TEmailDirection;
public status: interfaces.requests.TEmailStatus;
public from: string;
public to: string;
public subject: string;
public timestamp: string;
public messageId: string;
public size: string;
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.IEmail) {
this.clientRef = clientRef;
this.id = data.id;
this.direction = data.direction;
this.status = data.status;
this.from = data.from;
this.to = data.to;
this.subject = data.subject;
this.timestamp = data.timestamp;
this.messageId = data.messageId;
this.size = data.size;
}
public async getDetail(): Promise<interfaces.requests.IEmailDetail | null> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
'getEmailDetail',
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
);
return response.email;
}
public async resend(): Promise<{ success: boolean; newQueueId?: string }> {
const response = await this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
'resendEmail',
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
);
return response;
}
}
export class EmailManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<Email[]> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetAllEmails>(
'getAllEmails',
this.clientRef.buildRequestPayload() as any,
);
return response.emails.map((e) => new Email(this.clientRef, e));
}
public async getDetail(emailId: string): Promise<interfaces.requests.IEmailDetail | null> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
'getEmailDetail',
this.clientRef.buildRequestPayload({ emailId }) as any,
);
return response.email;
}
public async resend(emailId: string): Promise<{ success: boolean; newQueueId?: string }> {
return this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
'resendEmail',
this.clientRef.buildRequestPayload({ emailId }) as any,
);
}
}

View File

@@ -0,0 +1,37 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class LogManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async getRecent(options?: {
level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
limit?: number;
offset?: number;
search?: string;
timeRange?: string;
}): Promise<interfaces.requests.IReq_GetRecentLogs['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetRecentLogs>(
'getRecentLogs',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getStream(options?: {
follow?: boolean;
filters?: {
level?: string[];
category?: string[];
};
}): Promise<interfaces.requests.IReq_GetLogStream['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetLogStream>(
'getLogStream',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
}

View File

@@ -0,0 +1,180 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
// =====================
// Sub-managers
// =====================
export class RadiusClientManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<Array<{
name: string;
ipRange: string;
description?: string;
enabled: boolean;
}>> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusClients>(
'getRadiusClients',
this.clientRef.buildRequestPayload() as any,
);
return response.clients;
}
public async set(client: {
name: string;
ipRange: string;
secret: string;
description?: string;
enabled: boolean;
}): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_SetRadiusClient>(
'setRadiusClient',
this.clientRef.buildRequestPayload({ client }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to set RADIUS client');
}
}
public async remove(name: string): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRadiusClient>(
'removeRadiusClient',
this.clientRef.buildRequestPayload({ name }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to remove RADIUS client');
}
}
}
export class RadiusVlanManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<interfaces.requests.IReq_GetVlanMappings['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetVlanMappings>(
'getVlanMappings',
this.clientRef.buildRequestPayload() as any,
);
}
public async set(mapping: {
mac: string;
vlan: number;
description?: string;
enabled: boolean;
}): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_SetVlanMapping>(
'setVlanMapping',
this.clientRef.buildRequestPayload({ mapping }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to set VLAN mapping');
}
}
public async remove(mac: string): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveVlanMapping>(
'removeVlanMapping',
this.clientRef.buildRequestPayload({ mac }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to remove VLAN mapping');
}
}
public async updateConfig(options: {
defaultVlan?: number;
allowUnknownMacs?: boolean;
}): Promise<{ defaultVlan: number; allowUnknownMacs: boolean }> {
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateVlanConfig>(
'updateVlanConfig',
this.clientRef.buildRequestPayload(options) as any,
);
if (!response.success) {
throw new Error('Failed to update VLAN config');
}
return response.config;
}
public async testAssignment(mac: string): Promise<interfaces.requests.IReq_TestVlanAssignment['response']> {
return this.clientRef.request<interfaces.requests.IReq_TestVlanAssignment>(
'testVlanAssignment',
this.clientRef.buildRequestPayload({ mac }) as any,
);
}
}
export class RadiusSessionManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(filter?: {
username?: string;
nasIpAddress?: string;
vlanId?: number;
}): Promise<interfaces.requests.IReq_GetRadiusSessions['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetRadiusSessions>(
'getRadiusSessions',
this.clientRef.buildRequestPayload({ filter }) as any,
);
}
public async disconnect(sessionId: string, reason?: string): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_DisconnectRadiusSession>(
'disconnectRadiusSession',
this.clientRef.buildRequestPayload({ sessionId, reason }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to disconnect session');
}
}
}
// =====================
// Main RADIUS Manager
// =====================
export class RadiusManager {
private clientRef: DcRouterApiClient;
public clients: RadiusClientManager;
public vlans: RadiusVlanManager;
public sessions: RadiusSessionManager;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
this.clients = new RadiusClientManager(clientRef);
this.vlans = new RadiusVlanManager(clientRef);
this.sessions = new RadiusSessionManager(clientRef);
}
public async getAccountingSummary(
startTime: number,
endTime: number,
): Promise<interfaces.requests.IReq_GetRadiusAccountingSummary['response']['summary']> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusAccountingSummary>(
'getRadiusAccountingSummary',
this.clientRef.buildRequestPayload({ startTime, endTime }) as any,
);
return response.summary;
}
public async getStatistics(): Promise<interfaces.requests.IReq_GetRadiusStatistics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetRadiusStatistics>(
'getRadiusStatistics',
this.clientRef.buildRequestPayload() as any,
);
}
}

View File

@@ -0,0 +1,185 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class RemoteIngress {
private clientRef: DcRouterApiClient;
// Data from IRemoteIngress
public id: string;
public name: string;
public secret: string;
public listenPorts: number[];
public enabled: boolean;
public autoDerivePorts: boolean;
public tags?: string[];
public createdAt: number;
public updatedAt: number;
public effectiveListenPorts?: number[];
public manualPorts?: number[];
public derivedPorts?: number[];
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IRemoteIngress) {
this.clientRef = clientRef;
this.id = data.id;
this.name = data.name;
this.secret = data.secret;
this.listenPorts = data.listenPorts;
this.enabled = data.enabled;
this.autoDerivePorts = data.autoDerivePorts;
this.tags = data.tags;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
this.effectiveListenPorts = data.effectiveListenPorts;
this.manualPorts = data.manualPorts;
this.derivedPorts = data.derivedPorts;
}
public async update(changes: {
name?: string;
listenPorts?: number[];
autoDerivePorts?: boolean;
enabled?: boolean;
tags?: string[];
}): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRemoteIngress>(
'updateRemoteIngress',
this.clientRef.buildRequestPayload({ id: this.id, ...changes }) as any,
);
if (!response.success) {
throw new Error('Failed to update remote ingress');
}
// Update local state from response
const edge = response.edge;
this.name = edge.name;
this.listenPorts = edge.listenPorts;
this.enabled = edge.enabled;
this.autoDerivePorts = edge.autoDerivePorts;
this.tags = edge.tags;
this.updatedAt = edge.updatedAt;
this.effectiveListenPorts = edge.effectiveListenPorts;
this.manualPorts = edge.manualPorts;
this.derivedPorts = edge.derivedPorts;
}
public async delete(): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRemoteIngress>(
'deleteRemoteIngress',
this.clientRef.buildRequestPayload({ id: this.id }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to delete remote ingress');
}
}
public async regenerateSecret(): Promise<string> {
const response = await this.clientRef.request<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
'regenerateRemoteIngressSecret',
this.clientRef.buildRequestPayload({ id: this.id }) as any,
);
if (!response.success) {
throw new Error('Failed to regenerate secret');
}
this.secret = response.secret;
return response.secret;
}
public async getConnectionToken(hubHost?: string): Promise<string | undefined> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
'getRemoteIngressConnectionToken',
this.clientRef.buildRequestPayload({ edgeId: this.id, hubHost }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to get connection token');
}
return response.token;
}
}
export class RemoteIngressBuilder {
private clientRef: DcRouterApiClient;
private edgeName: string = '';
private edgeListenPorts?: number[];
private edgeAutoDerivePorts?: boolean;
private edgeTags?: string[];
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public setName(name: string): this {
this.edgeName = name;
return this;
}
public setListenPorts(ports: number[]): this {
this.edgeListenPorts = ports;
return this;
}
public setAutoDerivePorts(auto: boolean): this {
this.edgeAutoDerivePorts = auto;
return this;
}
public setTags(tags: string[]): this {
this.edgeTags = tags;
return this;
}
public async save(): Promise<RemoteIngress> {
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRemoteIngress>(
'createRemoteIngress',
this.clientRef.buildRequestPayload({
name: this.edgeName,
listenPorts: this.edgeListenPorts,
autoDerivePorts: this.edgeAutoDerivePorts,
tags: this.edgeTags,
}) as any,
);
if (!response.success) {
throw new Error('Failed to create remote ingress');
}
return new RemoteIngress(this.clientRef, response.edge);
}
}
export class RemoteIngressManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<RemoteIngress[]> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngresses>(
'getRemoteIngresses',
this.clientRef.buildRequestPayload() as any,
);
return response.edges.map((e) => new RemoteIngress(this.clientRef, e));
}
public async getStatuses(): Promise<interfaces.data.IRemoteIngressStatus[]> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressStatus>(
'getRemoteIngressStatus',
this.clientRef.buildRequestPayload() as any,
);
return response.statuses;
}
public async create(options: {
name: string;
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}): Promise<RemoteIngress> {
const builder = this.build().setName(options.name);
if (options.listenPorts) builder.setListenPorts(options.listenPorts);
if (options.autoDerivePorts !== undefined) builder.setAutoDerivePorts(options.autoDerivePorts);
if (options.tags) builder.setTags(options.tags);
return builder.save();
}
public build(): RemoteIngressBuilder {
return new RemoteIngressBuilder(this.clientRef);
}
}

View File

@@ -0,0 +1,203 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { IRouteConfig } from '@push.rocks/smartproxy';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
export class Route {
private clientRef: DcRouterApiClient;
// Data from IMergedRoute
public routeConfig: IRouteConfig;
public source: 'hardcoded' | 'programmatic';
public enabled: boolean;
public overridden: boolean;
public storedRouteId?: string;
public createdAt?: number;
public updatedAt?: number;
// Convenience accessors
public get name(): string {
return this.routeConfig.name || '';
}
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IMergedRoute) {
this.clientRef = clientRef;
this.routeConfig = data.route;
this.source = data.source;
this.enabled = data.enabled;
this.overridden = data.overridden;
this.storedRouteId = data.storedRouteId;
this.createdAt = data.createdAt;
this.updatedAt = data.updatedAt;
}
public async update(changes: Partial<IRouteConfig>): Promise<void> {
if (!this.storedRouteId) {
throw new Error('Cannot update a hardcoded route. Use setOverride() instead.');
}
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRoute>(
'updateRoute',
this.clientRef.buildRequestPayload({ id: this.storedRouteId, route: changes }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to update route');
}
}
public async delete(): Promise<void> {
if (!this.storedRouteId) {
throw new Error('Cannot delete a hardcoded route. Use setOverride() instead.');
}
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRoute>(
'deleteRoute',
this.clientRef.buildRequestPayload({ id: this.storedRouteId }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to delete route');
}
}
public async toggle(enabled: boolean): Promise<void> {
if (!this.storedRouteId) {
throw new Error('Cannot toggle a hardcoded route. Use setOverride() instead.');
}
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleRoute>(
'toggleRoute',
this.clientRef.buildRequestPayload({ id: this.storedRouteId, enabled }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to toggle route');
}
this.enabled = enabled;
}
public async setOverride(enabled: boolean): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_SetRouteOverride>(
'setRouteOverride',
this.clientRef.buildRequestPayload({ routeName: this.name, enabled }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to set route override');
}
this.overridden = true;
this.enabled = enabled;
}
public async removeOverride(): Promise<void> {
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRouteOverride>(
'removeRouteOverride',
this.clientRef.buildRequestPayload({ routeName: this.name }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to remove route override');
}
this.overridden = false;
}
}
export class RouteBuilder {
private clientRef: DcRouterApiClient;
private routeConfig: Partial<IRouteConfig> = {};
private isEnabled: boolean = true;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public setName(name: string): this {
this.routeConfig.name = name;
return this;
}
public setMatch(match: IRouteConfig['match']): this {
this.routeConfig.match = match;
return this;
}
public setAction(action: IRouteConfig['action']): this {
this.routeConfig.action = action;
return this;
}
public setTls(tls: IRouteConfig['action']['tls']): this {
if (!this.routeConfig.action) {
this.routeConfig.action = { type: 'forward' } as IRouteConfig['action'];
}
this.routeConfig.action!.tls = tls;
return this;
}
public setEnabled(enabled: boolean): this {
this.isEnabled = enabled;
return this;
}
public async save(): Promise<Route> {
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
'createRoute',
this.clientRef.buildRequestPayload({
route: this.routeConfig as IRouteConfig,
enabled: this.isEnabled,
}) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to create route');
}
// Return a Route instance by re-fetching the list
// The created route is programmatic, so we find it by storedRouteId
const { routes } = await new RouteManager(this.clientRef).list();
const created = routes.find((r) => r.storedRouteId === response.storedRouteId);
if (created) {
return created;
}
// Fallback: construct from known data
return new Route(this.clientRef, {
route: this.routeConfig as IRouteConfig,
source: 'programmatic',
enabled: this.isEnabled,
overridden: false,
storedRouteId: response.storedRouteId,
});
}
}
export class RouteManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async list(): Promise<{ routes: Route[]; warnings: interfaces.data.IRouteWarning[] }> {
const response = await this.clientRef.request<interfaces.requests.IReq_GetMergedRoutes>(
'getMergedRoutes',
this.clientRef.buildRequestPayload() as any,
);
return {
routes: response.routes.map((r) => new Route(this.clientRef, r)),
warnings: response.warnings,
};
}
public async create(routeConfig: IRouteConfig, enabled?: boolean): Promise<Route> {
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
'createRoute',
this.clientRef.buildRequestPayload({ route: routeConfig, enabled: enabled ?? true }) as any,
);
if (!response.success) {
throw new Error(response.message || 'Failed to create route');
}
return new Route(this.clientRef, {
route: routeConfig,
source: 'programmatic',
enabled: enabled ?? true,
overridden: false,
storedRouteId: response.storedRouteId,
});
}
public build(): RouteBuilder {
return new RouteBuilder(this.clientRef);
}
}

View File

@@ -0,0 +1,111 @@
import * as interfaces from '../ts_interfaces/index.js';
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
type TTimeRange = '1h' | '6h' | '24h' | '7d' | '30d';
export class StatsManager {
private clientRef: DcRouterApiClient;
constructor(clientRef: DcRouterApiClient) {
this.clientRef = clientRef;
}
public async getServer(options?: {
timeRange?: TTimeRange;
includeHistory?: boolean;
}): Promise<interfaces.requests.IReq_GetServerStatistics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetServerStatistics>(
'getServerStatistics',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getEmail(options?: {
timeRange?: TTimeRange;
domain?: string;
includeDetails?: boolean;
}): Promise<interfaces.requests.IReq_GetEmailStatistics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetEmailStatistics>(
'getEmailStatistics',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getDns(options?: {
timeRange?: TTimeRange;
domain?: string;
includeQueryTypes?: boolean;
}): Promise<interfaces.requests.IReq_GetDnsStatistics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetDnsStatistics>(
'getDnsStatistics',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getRateLimits(options?: {
domain?: string;
ip?: string;
includeBlocked?: boolean;
}): Promise<interfaces.requests.IReq_GetRateLimitStatus['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetRateLimitStatus>(
'getRateLimitStatus',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getSecurity(options?: {
timeRange?: TTimeRange;
includeDetails?: boolean;
}): Promise<interfaces.requests.IReq_GetSecurityMetrics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetSecurityMetrics>(
'getSecurityMetrics',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getConnections(options?: {
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
state?: string;
}): Promise<interfaces.requests.IReq_GetActiveConnections['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetActiveConnections>(
'getActiveConnections',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getQueues(options?: {
queueName?: string;
}): Promise<interfaces.requests.IReq_GetQueueStatus['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetQueueStatus>(
'getQueueStatus',
this.clientRef.buildRequestPayload(options || {}) as any,
);
}
public async getHealth(detailed?: boolean): Promise<interfaces.requests.IReq_GetHealthStatus['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetHealthStatus>(
'getHealthStatus',
this.clientRef.buildRequestPayload({ detailed }) as any,
);
}
public async getNetwork(): Promise<interfaces.requests.IReq_GetNetworkStats['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetNetworkStats>(
'getNetworkStats',
this.clientRef.buildRequestPayload() as any,
);
}
public async getCombined(sections?: {
server?: boolean;
email?: boolean;
dns?: boolean;
security?: boolean;
network?: boolean;
}): Promise<interfaces.requests.IReq_GetCombinedMetrics['response']> {
return this.clientRef.request<interfaces.requests.IReq_GetCombinedMetrics>(
'getCombinedMetrics',
this.clientRef.buildRequestPayload({ sections }) as any,
);
}
}

15
ts_apiclient/index.ts Normal file
View File

@@ -0,0 +1,15 @@
// Main client
export { DcRouterApiClient, type IDcRouterApiClientOptions } from './classes.dcrouterapiclient.js';
// Resource classes
export { Route, RouteBuilder, RouteManager } from './classes.route.js';
export { Certificate, CertificateManager, type ICertificateSummary } from './classes.certificate.js';
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
export { Email, EmailManager } from './classes.email.js';
// Read-only managers
export { StatsManager } from './classes.stats.js';
export { ConfigManager } from './classes.config.js';
export { LogManager } from './classes.logs.js';
export { RadiusManager, RadiusClientManager, RadiusVlanManager, RadiusSessionManager } from './classes.radius.js';

8
ts_apiclient/plugins.ts Normal file
View File

@@ -0,0 +1,8 @@
// @api.global scope
import * as typedrequest from '@api.global/typedrequest';
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
export {
typedrequest,
typedrequestInterfaces,
};

279
ts_apiclient/readme.md Normal file
View File

@@ -0,0 +1,279 @@
# @serve.zone/dcrouter-apiclient
A typed, object-oriented API client for DcRouter with a fluent builder pattern. 🔧
Programmatically manage your DcRouter instance — routes, certificates, API tokens, remote ingress edges, RADIUS, email operations, and more — all with full TypeScript type safety and an intuitive OO interface.
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Installation
```bash
pnpm add @serve.zone/dcrouter-apiclient
```
Or import directly from the main package:
```typescript
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
```
## Quick Start
```typescript
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
// Authenticate
await client.login('admin', 'password');
// List routes
const { routes, warnings } = await client.routes.list();
console.log(`${routes.length} routes, ${warnings.length} warnings`);
// Check health
const { health } = await client.stats.getHealth();
console.log(`Healthy: ${health.healthy}`);
```
## Usage
### 🔐 Authentication
```typescript
// Login with credentials — identity is stored and auto-injected into all subsequent requests
const identity = await client.login('admin', 'password');
// Verify current session
const { valid } = await client.verifyIdentity();
// Logout
await client.logout();
// Or use an API token for programmatic access (route management only)
const client = new DcRouterApiClient({
baseUrl: 'https://dcrouter.example.com',
apiToken: 'dcr_your_token_here',
});
```
### 🌐 Routes — OO Resources + Builder
Routes are returned as `Route` instances with methods for update, delete, toggle, and overrides:
```typescript
// List all routes (hardcoded + programmatic)
const { routes, warnings } = await client.routes.list();
// Inspect a route
const route = routes[0];
console.log(route.name, route.source, route.enabled);
// Modify a programmatic route
await route.update({ name: 'renamed-route' });
await route.toggle(false);
await route.delete();
// Override a hardcoded route (disable it)
const hardcodedRoute = routes.find(r => r.source === 'hardcoded');
await hardcodedRoute.setOverride(false);
await hardcodedRoute.removeOverride();
```
**Builder pattern** for creating new routes:
```typescript
const newRoute = await client.routes.build()
.setName('api-gateway')
.setMatch({ ports: 443, domains: ['api.example.com'] })
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
.setTls({ mode: 'terminate', certificate: 'auto' })
.setEnabled(true)
.save();
// Or use quick creation
const route = await client.routes.create(routeConfig);
```
### 🔑 API Tokens
```typescript
// List existing tokens
const tokens = await client.apiTokens.list();
// Create with builder
const token = await client.apiTokens.build()
.setName('ci-pipeline')
.setScopes(['routes:read', 'routes:write'])
.addScope('config:read')
.setExpiresInDays(90)
.save();
console.log(token.tokenValue); // Only available at creation time!
// Manage tokens
await token.toggle(false); // Disable
const newValue = await token.roll(); // Regenerate secret
await token.revoke(); // Delete
```
### 🔐 Certificates
```typescript
const { certificates, summary } = await client.certificates.list();
console.log(`${summary.valid} valid, ${summary.expiring} expiring, ${summary.failed} failed`);
// Operate on individual certificates
const cert = certificates[0];
await cert.reprovision();
const exported = await cert.export();
await cert.delete();
// Import a certificate
await client.certificates.import({
id: 'cert-id',
domainName: 'example.com',
created: Date.now(),
validUntil: Date.now() + 90 * 24 * 3600 * 1000,
privateKey: '...',
publicKey: '...',
csr: '...',
});
```
### 🌍 Remote Ingress
```typescript
// List edges and their statuses
const edges = await client.remoteIngress.list();
const statuses = await client.remoteIngress.getStatuses();
// Create with builder
const edge = await client.remoteIngress.build()
.setName('edge-nyc-01')
.setListenPorts([80, 443])
.setAutoDerivePorts(true)
.setTags(['us-east'])
.save();
// Manage an edge
await edge.update({ name: 'edge-nyc-02' });
const newSecret = await edge.regenerateSecret();
const token = await edge.getConnectionToken();
await edge.delete();
```
### 📊 Statistics (Read-Only)
```typescript
const serverStats = await client.stats.getServer({ timeRange: '24h', includeHistory: true });
const emailStats = await client.stats.getEmail({ domain: 'example.com' });
const dnsStats = await client.stats.getDns();
const security = await client.stats.getSecurity({ includeDetails: true });
const connections = await client.stats.getConnections({ protocol: 'https' });
const queues = await client.stats.getQueues();
const health = await client.stats.getHealth(true);
const network = await client.stats.getNetwork();
const combined = await client.stats.getCombined({ server: true, email: true });
```
### ⚙️ Configuration & Logs
```typescript
// Read-only configuration
const config = await client.config.get();
const emailSection = await client.config.get('email');
// Logs
const { logs, total, hasMore } = await client.logs.getRecent({
level: 'error',
category: 'smtp',
limit: 50,
});
```
### 📧 Email Operations
```typescript
const emails = await client.emails.list();
const email = emails[0];
const detail = await email.getDetail();
await email.resend();
// Or use the manager directly
const detail2 = await client.emails.getDetail('email-id');
await client.emails.resend('email-id');
```
### 📡 RADIUS
```typescript
// Client management
const clients = await client.radius.clients.list();
await client.radius.clients.set({
name: 'switch-1',
ipRange: '192.168.1.0/24',
secret: 'shared-secret',
enabled: true,
});
await client.radius.clients.remove('switch-1');
// VLAN management
const { mappings, config: vlanConfig } = await client.radius.vlans.list();
await client.radius.vlans.set({ mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true });
const result = await client.radius.vlans.testAssignment('aa:bb:cc:dd:ee:ff');
await client.radius.vlans.updateConfig({ defaultVlan: 200 });
// Sessions
const { sessions } = await client.radius.sessions.list({ vlanId: 10 });
await client.radius.sessions.disconnect('session-id', 'Admin disconnect');
// Statistics & Accounting
const stats = await client.radius.getStatistics();
const summary = await client.radius.getAccountingSummary(startTime, endTime);
```
## API Surface
| Manager | Methods |
|---------|---------|
| `client.login()` / `logout()` / `verifyIdentity()` | Authentication |
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getRateLimits()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
| `client.config` | `get(section?)` |
| `client.logs` | `getRecent()`, `getStream()` |
| `client.emails` | `list()`, `getDetail()`, `resend()` → Email: `getDetail()`, `resend()` |
| `client.radius` | `.clients.list/set/remove()`, `.vlans.list/set/remove/updateConfig/testAssignment()`, `.sessions.list/disconnect()`, `getStatistics()`, `getAccountingSummary()` |
## Architecture
The client uses HTTP-based [TypedRequest](https://code.foss.global/api.global/typedrequest) for transport. All requests are sent as POST to `{baseUrl}/typedrequest`. Authentication (JWT identity and/or API token) is automatically injected into every request payload via `buildRequestPayload()`.
Resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) hold a reference to the client and provide instance methods that fire the appropriate TypedRequest operations. Builder classes (`RouteBuilder`, `ApiTokenBuilder`, `RemoteIngressBuilder`) use fluent chaining and a terminal `.save()` method.
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -0,0 +1,3 @@
{
"order": 4
}

View File

@@ -82,6 +82,14 @@ interface IIdentity {
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
| `ILogEntry` | Timestamp, level, category, message, metadata |
#### Route Management Interfaces
| Interface | Description |
|-----------|-------------|
| `IMergedRoute` | Combined route: routeConfig, source (hardcoded/programmatic), enabled, overridden |
| `IRouteWarning` | Merge warning: disabled-hardcoded, disabled-programmatic, orphaned-override |
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
#### Remote Ingress Interfaces
| Interface | Description |
|-----------|-------------|
@@ -128,13 +136,29 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
#### 📧 Email Operations
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetQueuedEmails` | `getQueuedEmails` | List queued emails |
| `IReq_GetSentEmails` | `getSentEmails` | List delivered emails |
| `IReq_GetFailedEmails` | `getFailedEmails` | List failed emails |
| `IReq_GetAllEmails` | `getAllEmails` | List all emails |
| `IReq_GetEmailDetail` | `getEmailDetail` | Full detail for a specific email |
| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email |
| `IReq_GetSecurityIncidents` | `getSecurityIncidents` | Security events |
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
#### 🛣️ Route Management
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetMergedRoutes` | `getMergedRoutes` | List all routes (hardcoded + programmatic) |
| `IReq_CreateRoute` | `createRoute` | Create a new programmatic route |
| `IReq_UpdateRoute` | `updateRoute` | Update a programmatic route |
| `IReq_DeleteRoute` | `deleteRoute` | Delete a programmatic route |
| `IReq_ToggleRoute` | `toggleRoute` | Enable/disable a programmatic route |
| `IReq_SetRouteOverride` | `setRouteOverride` | Override a hardcoded route |
| `IReq_RemoveRouteOverride` | `removeRouteOverride` | Remove a route override |
#### 🔑 API Token Management
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_CreateApiToken` | `createApiToken` | Create a new API token |
| `IReq_ListApiTokens` | `listApiTokens` | List all tokens |
| `IReq_RevokeApiToken` | `revokeApiToken` | Revoke (delete) a token |
| `IReq_RollApiToken` | `rollApiToken` | Regenerate token secret |
| `IReq_ToggleApiToken` | `toggleApiToken` | Enable/disable a token |
#### 🔐 Certificates
| Interface | Method | Description |
@@ -198,6 +222,8 @@ interface ICertificateInfo {
## Example: Full API Integration
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.
```typescript
import * as typedrequest from '@api.global/typedrequest';
import { data, requests } from '@serve.zone/dcrouter-interfaces';

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ export interface IConfigData {
enabled: boolean;
tunnelPort: number | null;
hubDomain: string | null;
tlsConfigured: boolean;
tlsMode: 'custom' | 'acme' | 'self-signed';
connectedEdgeIps: string[];
};
}
@@ -81,7 +81,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
> {
method: 'getConfiguration';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
section?: string;
};
response: {

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ export interface IReq_GetServerStatistics extends plugins.typedrequestInterfaces
> {
method: 'getServerStatistics';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
includeHistory?: boolean;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
};
@@ -29,7 +29,7 @@ export interface IReq_GetEmailStatistics extends plugins.typedrequestInterfaces.
> {
method: 'getEmailStatistics';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string;
includeDetails?: boolean;
@@ -49,7 +49,7 @@ export interface IReq_GetDnsStatistics extends plugins.typedrequestInterfaces.im
> {
method: 'getDnsStatistics';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
domain?: string;
includeQueryTypes?: boolean;
@@ -69,7 +69,7 @@ export interface IReq_GetRateLimitStatus extends plugins.typedrequestInterfaces.
> {
method: 'getRateLimitStatus';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
domain?: string;
ip?: string;
includeBlocked?: boolean;
@@ -91,7 +91,7 @@ export interface IReq_GetSecurityMetrics extends plugins.typedrequestInterfaces.
> {
method: 'getSecurityMetrics';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
includeDetails?: boolean;
};
@@ -112,7 +112,7 @@ export interface IReq_GetActiveConnections extends plugins.typedrequestInterface
> {
method: 'getActiveConnections';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
state?: string;
};
@@ -137,7 +137,7 @@ export interface IReq_GetQueueStatus extends plugins.typedrequestInterfaces.impl
> {
method: 'getQueueStatus';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
queueName?: string;
};
response: {
@@ -153,10 +153,31 @@ export interface IReq_GetHealthStatus extends plugins.typedrequestInterfaces.imp
> {
method: 'getHealthStatus';
request: {
identity?: authInterfaces.IIdentity;
identity: authInterfaces.IIdentity;
detailed?: boolean;
};
response: {
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 = {
name: '@serve.zone/dcrouter',
version: '9.3.0',
version: '11.2.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -238,9 +238,12 @@ interface IActionContext {
}
const getActionContext = (): IActionContext => {
return {
identity: loginStatePart.getState().identity,
};
const 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
@@ -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) => {
const context = getActionContext();
if (!context.identity) return statePartArg.getState();
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_AdminLogout
>('/typedrequest', 'adminLogout');
try {
await typedRequest.fire({
identity: context.identity,
});
} catch (error) {
console.error('Logout error:', error);
// Try to notify server, but don't block logout if identity is missing/expired
if (context.identity) {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_AdminLogout
>('/typedrequest', 'adminLogout');
try {
await typedRequest.fire({ identity: context.identity });
} catch (error) {
console.error('Logout error:', error);
}
}
// Clear login state regardless
// Always clear login state
return {
identity: null,
isLoggedIn: false,
@@ -298,8 +300,8 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
// Fetch All Stats Action - Using combined endpoint for efficiency
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
// 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)
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -373,6 +375,7 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
if (!context.identity) return statePartArg.getState();
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetRecentLogs
@@ -448,8 +451,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
// Fetch Network Stats Action
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
// 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) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
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) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -581,7 +586,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
});
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain) => {
async (statePartArg, domain, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -596,8 +601,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
});
// Re-fetch overview after reprovisioning
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return {
...currentState,
@@ -608,7 +612,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
);
export const deleteCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain) => {
async (statePartArg, domain, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -623,8 +627,7 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
});
// Re-fetch overview after deletion
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return {
...currentState,
@@ -643,7 +646,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
publicKey: string;
csr: string;
}>(
async (statePartArg, cert) => {
async (statePartArg, cert, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -658,8 +661,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
});
// Re-fetch overview after import
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return {
...currentState,
@@ -700,6 +702,7 @@ export async function fetchConnectionToken(edgeId: string) {
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -737,7 +740,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -756,7 +759,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
if (response.success) {
// Refresh the list
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
await actionContext.dispatch(fetchRemoteIngressAction, null);
return {
...statePartArg.getState(),
@@ -774,7 +777,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
});
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
async (statePartArg, edgeId) => {
async (statePartArg, edgeId, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -788,8 +791,7 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
id: edgeId,
});
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return {
...currentState,
@@ -805,7 +807,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -823,8 +825,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
tags: dataArg.tags,
});
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return {
...currentState,
@@ -877,7 +878,7 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -892,8 +893,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
enabled: dataArg.enabled,
});
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return {
...currentState,
@@ -909,6 +909,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -939,7 +940,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{
route: any;
enabled?: boolean;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -954,8 +955,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled,
});
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return {
...currentState,
@@ -965,7 +965,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
});
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeId) => {
async (statePartArg, routeId, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -979,8 +979,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
id: routeId,
});
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return {
...currentState,
@@ -993,7 +992,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
export const toggleRouteAction = routeManagementStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -1008,8 +1007,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled,
});
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return {
...currentState,
@@ -1021,7 +1019,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
routeName: string;
enabled: boolean;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -1036,8 +1034,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled,
});
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return {
...currentState,
@@ -1047,7 +1044,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
});
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeName) => {
async (statePartArg, routeName, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -1061,8 +1058,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
routeName,
});
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return {
...currentState,
@@ -1079,6 +1075,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
if (!context.identity) return currentState;
try {
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>(
async (statePartArg, tokenId) => {
async (statePartArg, tokenId, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -1130,8 +1139,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
id: tokenId,
});
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchApiTokensAction, null);
} catch (error) {
return {
...currentState,
@@ -1144,7 +1152,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg, actionContext) => {
const context = getActionContext();
const currentState = statePartArg.getState();
@@ -1159,8 +1167,7 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
enabled: dataArg.enabled,
});
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
return statePartArg.getState();
return await actionContext.dispatch(fetchApiTokensAction, null);
} catch (error) {
return {
...currentState,
@@ -1221,8 +1228,9 @@ async function disconnectSocket() {
// Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() {
const context = getActionContext();
if (!context.identity) return;
const currentView = uiStatePart.getState().activeView;
try {
// Always fetch basic stats for dashboard widgets
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1332,6 +1340,12 @@ async function dispatchCombinedRefreshAction() {
}
} catch (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 appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { appRouter } from '../router.js';
import {
@@ -218,13 +219,27 @@ export class OpsDashboard extends DeesElement {
// Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState();
if (loginState.identity?.jwt) {
// Verify JWT hasn't expired
if (loginState.identity.expiresAt > Date.now()) {
// JWT still valid, restore logged-in state
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
// Client-side expiry looks valid — verify with server (keypair may have changed)
try {
const verifyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_VerifyIdentity
>('/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 {
// JWT expired, clear the stored state
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',
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() {
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
}

View File

@@ -300,7 +300,7 @@ export class OpsViewConfig extends DeesElement {
const fields: IConfigField[] = [
{ key: 'Tunnel Port', value: ri.tunnelPort },
{ 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' },
];

View File

@@ -76,8 +76,15 @@ export class OpsViewLogs extends DeesElement {
// Wait for xterm terminal to finish initializing (CDN load)
if (!chartLog.terminalReady) {
await new Promise<void>((resolve) => {
let attempts = 0;
const maxAttempts = 200; // 200 * 50ms = 10 seconds
const check = () => {
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);
};
check();

View File

@@ -55,6 +55,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- Filter by log level (error, warning, info, debug)
- Search and time-range selection
### 🛣️ Route & API Token Management
- Programmatic route CRUD with enable/disable and override controls
- API token creation, revocation, and scope management
- Routes tab and API Tokens tab in unified view
### ⚙️ Configuration
- Read-only display of current system configuration
- Status badges for boolean values (enabled/disabled)
@@ -96,6 +101,7 @@ ts_web/
├── ops-view-certificates.ts # Certificate overview & reprovisioning
├── ops-view-remoteingress.ts # Remote ingress edge management
├── ops-view-logs.ts # Log viewer
├── ops-view-routes.ts # Route & API token management
├── ops-view-config.ts # Configuration display
├── ops-view-security.ts # Security dashboard
└── shared/
@@ -171,6 +177,7 @@ fetchConnectionToken(edgeId) // Get connection token (standalone function)
/emails/security → Security incidents
/certificates → Certificate management
/remoteingress → Remote ingress edge management
/routes → Route & API token management
/logs → Log viewer
/configuration → System configuration
/security → Security dashboard