Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cb1f3c12d | |||
| c7d7f92759 | |||
| 02e1b9231f | |||
| 4ec4dd2bdb | |||
| aa543160e2 | |||
| 94fa0f04d8 | |||
| 17deb481e0 | |||
| e452ffd38e | |||
| 865b4a53e6 | |||
| c07f3975e9 | |||
| 476505537a | |||
| 74ad5cec90 | |||
| 59a3f7978e | |||
| 7dc976b59e |
6
.playwright-mcp/console-2026-03-02T19-29-32-708Z.log
Normal file
6
.playwright-mcp/console-2026-03-02T19-29-32-708Z.log
Normal file
@@ -0,0 +1,6 @@
|
||||
[ 95ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
||||
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
||||
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
||||
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
||||
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
||||
[ 992ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||
5
.playwright-mcp/console-2026-03-02T19-30-09-759Z.log
Normal file
5
.playwright-mcp/console-2026-03-02T19-30-09-759Z.log
Normal file
@@ -0,0 +1,5 @@
|
||||
[ 329ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 727ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||
[ 260513ms] [ERROR] method: >>adminLoginWithUsernameAndPassword<< got an ERROR: "login failed" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 260514ms] [ERROR] Login failed: Ns @ http://localhost:3000/bundle.js:38066
|
||||
[ 260518ms] [WARNING] FontAwesome icon not found: circle-xmark @ http://localhost:3000/bundle.js:1203
|
||||
3
.playwright-mcp/console-2026-03-02T19-34-55-496Z.log
Normal file
3
.playwright-mcp/console-2026-03-02T19-34-55-496Z.log
Normal file
@@ -0,0 +1,3 @@
|
||||
[ 397ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||
[ 657ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||
[ 24180ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
||||
BIN
.playwright-mcp/page-2026-03-02T19-32-32-890Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-02T19-32-32-890Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
.playwright-mcp/page-2026-03-02T19-33-32-637Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-02T19-33-32-637Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
56
changelog.md
56
changelog.md
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "10.1.0",
|
||||
"version": "10.1.7",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -24,10 +24,10 @@
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.2.0",
|
||||
"@types/node": "^25.3.0"
|
||||
"@types/node": "^25.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.2.6",
|
||||
"@api.global/typedrequest": "^3.2.7",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^8.4.0",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@push.rocks/smartlog": "^3.2.1",
|
||||
"@push.rocks/smartmetrics": "^3.0.1",
|
||||
"@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",
|
||||
@@ -53,7 +53,7 @@
|
||||
"@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",
|
||||
|
||||
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@api.global/typedrequest':
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6
|
||||
specifier: ^3.2.7
|
||||
version: 3.2.7
|
||||
'@api.global/typedrequest-interfaces':
|
||||
specifier: ^3.0.19
|
||||
version: 3.0.19
|
||||
@@ -63,8 +63,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(socks@2.8.7)
|
||||
'@push.rocks/smartmta':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1
|
||||
'@push.rocks/smartnetwork':
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@@ -87,8 +87,8 @@ importers:
|
||||
specifier: ^3.0.10
|
||||
version: 3.0.10
|
||||
'@push.rocks/smartstate':
|
||||
specifier: ^2.0.30
|
||||
version: 2.0.30
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@push.rocks/smartunique':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
@@ -127,8 +127,8 @@ importers:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(@tiptap/pm@2.27.2)
|
||||
'@types/node':
|
||||
specifier: ^25.3.0
|
||||
version: 25.3.0
|
||||
specifier: ^25.3.3
|
||||
version: 25.3.3
|
||||
|
||||
packages:
|
||||
|
||||
@@ -138,8 +138,8 @@ packages:
|
||||
'@api.global/typedrequest-interfaces@3.0.19':
|
||||
resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==}
|
||||
|
||||
'@api.global/typedrequest@3.2.6':
|
||||
resolution: {integrity: sha512-CnvbjYjnGGw3rwL+7bTHSgRHEpDujzhs3cv7l1xgCXMPQe3DcPg74+9ep1Y5cu21T/w0pxNnDCJpbb0SHqHzAw==}
|
||||
'@api.global/typedrequest@3.2.7':
|
||||
resolution: {integrity: sha512-9CC8EojPDraKlwWK3ZjM8/wJ9jguY/kc+pCgcd61epHFXTIKC8jYts3vKPmEkBPno5Ejn3JZgqp/GRzplCC51w==}
|
||||
|
||||
'@api.global/typedserver@3.0.80':
|
||||
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
|
||||
@@ -845,6 +845,9 @@ packages:
|
||||
'@push.rocks/lik@6.2.2':
|
||||
resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==}
|
||||
|
||||
'@push.rocks/lik@6.3.1':
|
||||
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
|
||||
|
||||
'@push.rocks/mongodump@1.1.0':
|
||||
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
|
||||
|
||||
@@ -996,8 +999,8 @@ packages:
|
||||
'@push.rocks/smartmongo@5.1.0':
|
||||
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
|
||||
|
||||
'@push.rocks/smartmta@5.3.0':
|
||||
resolution: {integrity: sha512-uJI25fslzvrcenU36WCdt5gB8cCfkjUlY7PqlxEtFp474/l/kZxNnvirv1gnZLRNNa+ioe5aH18HKE+KcAjuxA==}
|
||||
'@push.rocks/smartmta@5.3.1':
|
||||
resolution: {integrity: sha512-cEuXO56i/zL9eZS79eAesEW16ikdBJKLlEv9pLKkt2cmaHBWADGHjeOzJmsszQ9CSFcuhd41aHYVGMZXVvsG2g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
@@ -1083,8 +1086,8 @@ packages:
|
||||
'@push.rocks/smartspawn@3.0.3':
|
||||
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
|
||||
|
||||
'@push.rocks/smartstate@2.0.30':
|
||||
resolution: {integrity: sha512-IuNW8XtSumXIr7g7MIFyWg5PBwLF2mwsymTJbSEycK2Pa9ZLk4yjRHnR907xCilxgiMU9ixQZyNdpa5MMF999A==}
|
||||
'@push.rocks/smartstate@2.2.0':
|
||||
resolution: {integrity: sha512-e41vA1y9b0HBauzjMSh3l0YlRhcG4jhArm43/HHNdT+inxEGIeRL24VGeq+sl2MUr/eFWqgrETXhvL3YrsYFaw==}
|
||||
|
||||
'@push.rocks/smartstream@2.0.8':
|
||||
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
|
||||
@@ -1835,11 +1838,11 @@ packages:
|
||||
'@types/node@18.19.130':
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
|
||||
'@types/node@22.19.11':
|
||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||
'@types/node@22.19.13':
|
||||
resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==}
|
||||
|
||||
'@types/node@25.3.0':
|
||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||
'@types/node@25.3.3':
|
||||
resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
|
||||
|
||||
'@types/ping@0.4.4':
|
||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||
@@ -4291,11 +4294,11 @@ snapshots:
|
||||
|
||||
'@api.global/typedrequest-interfaces@3.0.19': {}
|
||||
|
||||
'@api.global/typedrequest@3.2.6':
|
||||
'@api.global/typedrequest@3.2.7':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/isounique': 1.0.5
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/lik': 6.3.1
|
||||
'@push.rocks/smartbuffer': 3.0.5
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartguard': 3.1.0
|
||||
@@ -4305,7 +4308,7 @@ snapshots:
|
||||
|
||||
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260210.0
|
||||
@@ -4353,7 +4356,7 @@ snapshots:
|
||||
|
||||
'@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260303.0
|
||||
@@ -4399,7 +4402,7 @@ snapshots:
|
||||
|
||||
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/isohash': 2.0.1
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
@@ -4419,7 +4422,7 @@ snapshots:
|
||||
|
||||
'@api.global/typedsocket@4.1.2(@push.rocks/smartserve@2.0.1)':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/isohash': 2.0.1
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4994,14 +4997,14 @@ snapshots:
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
broadcast-channel: 7.3.0
|
||||
|
||||
'@design.estate/dees-domtools@2.3.8':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -5010,7 +5013,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrouter': 1.3.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstate': 2.0.30
|
||||
'@push.rocks/smartstate': 2.2.0
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
@@ -5334,7 +5337,7 @@ snapshots:
|
||||
'@inquirer/figures': 1.0.15
|
||||
'@inquirer/type': 2.0.0
|
||||
'@types/mute-stream': 0.0.4
|
||||
'@types/node': 22.19.11
|
||||
'@types/node': 22.19.13
|
||||
'@types/wrap-ansi': 3.0.0
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
@@ -5676,7 +5679,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/levelcache@3.2.0':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/lik': 6.3.1
|
||||
'@push.rocks/smartbucket': 3.3.10
|
||||
'@push.rocks/smartcache': 1.0.18
|
||||
'@push.rocks/smartenv': 5.0.13
|
||||
@@ -5707,6 +5710,17 @@ snapshots:
|
||||
'@types/symbol-tree': 3.2.5
|
||||
symbol-tree: 3.2.4
|
||||
|
||||
'@push.rocks/lik@6.3.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartmatch': 2.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smarttime': 4.2.3
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/symbol-tree': 3.2.5
|
||||
symbol-tree: 3.2.4
|
||||
|
||||
'@push.rocks/mongodump@1.1.0(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
@@ -5751,7 +5765,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@configvault.io/interfaces': 1.0.17
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartlog': 3.2.1
|
||||
@@ -6233,7 +6247,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartmta@5.3.0':
|
||||
'@push.rocks/smartmta@5.3.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartfs': 1.3.1
|
||||
@@ -6424,7 +6438,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartserve@2.0.1':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.2.6
|
||||
'@api.global/typedrequest': 3.2.7
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartenv': 6.0.0
|
||||
@@ -6487,9 +6501,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartstate@2.0.30':
|
||||
'@push.rocks/smartstate@2.2.0':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smarthash': 3.2.6
|
||||
'@push.rocks/smartjson': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@@ -7359,22 +7372,22 @@ snapshots:
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@@ -7382,7 +7395,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
@@ -7395,17 +7408,17 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.6':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7427,12 +7440,12 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
@@ -7455,26 +7468,26 @@ snapshots:
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/node@18.19.130':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.19.11':
|
||||
'@types/node@22.19.13':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.3.0':
|
||||
'@types/node@25.3.3':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
@@ -7492,22 +7505,22 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -7537,11 +7550,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@@ -8018,7 +8031,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.19
|
||||
'@types/node': 25.3.0
|
||||
'@types/node': 25.3.3
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '10.1.0',
|
||||
version: '10.1.7',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -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,6 +1375,14 @@ 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 {
|
||||
@@ -1371,8 +1391,10 @@ export class DcRouter {
|
||||
await (this.dnsServer as any).handleHttpsSocket(socket);
|
||||
} catch (error) {
|
||||
logger.log('error', `DNS socket handler error: ${error.message}`);
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +97,8 @@ 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;
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -183,6 +183,13 @@ 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
|
||||
|
||||
@@ -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 = [
|
||||
@@ -144,6 +146,19 @@ 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
|
||||
*/
|
||||
|
||||
@@ -84,6 +84,13 @@ 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,40 +250,34 @@ 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())
|
||||
@@ -288,12 +290,6 @@ export class SecurityLogger {
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
total: events.length,
|
||||
byLevel,
|
||||
byType,
|
||||
topIPs,
|
||||
topDomains
|
||||
};
|
||||
return { total, byLevel, byType, topIPs, topDomains };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '10.1.0',
|
||||
version: '10.1.7',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -581,7 +581,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 +596,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 +607,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 +622,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 +641,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 +656,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,
|
||||
@@ -737,7 +734,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 +753,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 +771,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 +785,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 +801,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 +819,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 +872,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 +887,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,
|
||||
@@ -939,7 +933,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 +948,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 +958,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 +972,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 +985,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 +1000,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 +1012,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 +1027,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 +1037,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 +1051,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,
|
||||
@@ -1128,7 +1117,7 @@ export async function rollApiToken(id: string) {
|
||||
}
|
||||
|
||||
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, tokenId) => {
|
||||
async (statePartArg, tokenId, actionContext) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
@@ -1142,8 +1131,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,
|
||||
@@ -1156,7 +1144,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();
|
||||
|
||||
@@ -1171,8 +1159,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,
|
||||
|
||||
@@ -154,7 +154,7 @@ export class OpsViewApiTokens extends DeesElement {
|
||||
},
|
||||
{
|
||||
name: 'Roll',
|
||||
iconName: 'lucide:refresh-cw',
|
||||
iconName: 'lucide:rotateCw',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||
@@ -306,7 +306,7 @@ export class OpsViewApiTokens extends DeesElement {
|
||||
},
|
||||
{
|
||||
name: 'Roll Token',
|
||||
iconName: 'lucide:refresh-cw',
|
||||
iconName: 'lucide:rotateCw',
|
||||
action: async (modalArg: any) => {
|
||||
await modalArg.destroy();
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user