Compare commits

...

20 Commits

Author SHA1 Message Date
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
23 changed files with 554 additions and 207 deletions

View File

@@ -0,0 +1,6 @@
[ 95ms] TypeError: Cannot read properties of null (reading 'appendChild')
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
[ 992ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0

View File

@@ -0,0 +1,5 @@
[ 329ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 727ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
[ 260513ms] [ERROR] method: >>adminLoginWithUsernameAndPassword<< got an ERROR: "login failed" with data undefined @ http://localhost:3000/bundle.js:13
[ 260514ms] [ERROR] Login failed: Ns @ http://localhost:3000/bundle.js:38066
[ 260518ms] [WARNING] FontAwesome icon not found: circle-xmark @ http://localhost:3000/bundle.js:1203

View File

@@ -0,0 +1,3 @@
[ 397ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
[ 657ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
[ 24180ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,84 @@
# Changelog # Changelog
## 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) ## 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 replace tlsConfigured boolean with tlsMode ('custom' | 'acme' | 'self-signed') and compute TLS mode server-side

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "10.0.0", "version": "10.1.9",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -24,10 +24,10 @@
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "^3.2.0", "@git.zone/tswatch": "^3.2.0",
"@types/node": "^25.3.0" "@types/node": "^25.3.3"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.2.6", "@api.global/typedrequest": "^3.2.7",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.4.0", "@api.global/typedserver": "^8.4.0",
"@api.global/typedsocket": "^4.1.2", "@api.global/typedsocket": "^4.1.2",
@@ -43,21 +43,21 @@
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.1", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.1", "@push.rocks/smartmetrics": "^3.0.2",
"@push.rocks/smartmongo": "^5.1.0", "@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.3.0", "@push.rocks/smartmta": "^5.3.1",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^25.8.5", "@push.rocks/smartproxy": "^25.9.1",
"@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.0.30", "@push.rocks/smartstate": "^2.2.0",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@serve.zone/catalog": "^2.5.0", "@serve.zone/catalog": "^2.5.0",
"@serve.zone/interfaces": "^5.3.0", "@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.3.0", "@serve.zone/remoteingress": "^4.4.0",
"@tsclass/tsclass": "^9.3.0", "@tsclass/tsclass": "^9.3.0",
"lru-cache": "^11.2.6", "lru-cache": "^11.2.6",
"uuid": "^13.0.0" "uuid": "^13.0.0"

171
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@api.global/typedrequest': '@api.global/typedrequest':
specifier: ^3.2.6 specifier: ^3.2.7
version: 3.2.6 version: 3.2.7
'@api.global/typedrequest-interfaces': '@api.global/typedrequest-interfaces':
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
@@ -57,14 +57,14 @@ importers:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.2.1 version: 3.2.1
'@push.rocks/smartmetrics': '@push.rocks/smartmetrics':
specifier: ^3.0.1 specifier: ^3.0.2
version: 3.0.1 version: 3.0.2
'@push.rocks/smartmongo': '@push.rocks/smartmongo':
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(socks@2.8.7) version: 5.1.0(socks@2.8.7)
'@push.rocks/smartmta': '@push.rocks/smartmta':
specifier: ^5.3.0 specifier: ^5.3.1
version: 5.3.0 version: 5.3.1
'@push.rocks/smartnetwork': '@push.rocks/smartnetwork':
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^25.8.5 specifier: ^25.9.1
version: 25.8.5 version: 25.9.1
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -87,8 +87,8 @@ importers:
specifier: ^3.0.10 specifier: ^3.0.10
version: 3.0.10 version: 3.0.10
'@push.rocks/smartstate': '@push.rocks/smartstate':
specifier: ^2.0.30 specifier: ^2.2.0
version: 2.0.30 version: 2.2.0
'@push.rocks/smartunique': '@push.rocks/smartunique':
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
@@ -99,8 +99,8 @@ importers:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
'@serve.zone/remoteingress': '@serve.zone/remoteingress':
specifier: ^4.3.0 specifier: ^4.4.0
version: 4.3.0 version: 4.4.0
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -127,8 +127,8 @@ importers:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(@tiptap/pm@2.27.2) version: 3.2.0(@tiptap/pm@2.27.2)
'@types/node': '@types/node':
specifier: ^25.3.0 specifier: ^25.3.3
version: 25.3.0 version: 25.3.3
packages: packages:
@@ -138,8 +138,8 @@ packages:
'@api.global/typedrequest-interfaces@3.0.19': '@api.global/typedrequest-interfaces@3.0.19':
resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==} resolution: {integrity: sha512-uuHUXJeOy/inWSDrwD0Cwax2rovpxYllDhM2RWh+6mVpQuNmZ3uw6IVg6dA2G1rOe24Ebs+Y9SzEogo+jYN7vw==}
'@api.global/typedrequest@3.2.6': '@api.global/typedrequest@3.2.7':
resolution: {integrity: sha512-CnvbjYjnGGw3rwL+7bTHSgRHEpDujzhs3cv7l1xgCXMPQe3DcPg74+9ep1Y5cu21T/w0pxNnDCJpbb0SHqHzAw==} resolution: {integrity: sha512-9CC8EojPDraKlwWK3ZjM8/wJ9jguY/kc+pCgcd61epHFXTIKC8jYts3vKPmEkBPno5Ejn3JZgqp/GRzplCC51w==}
'@api.global/typedserver@3.0.80': '@api.global/typedserver@3.0.80':
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==} resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
@@ -845,6 +845,9 @@ packages:
'@push.rocks/lik@6.2.2': '@push.rocks/lik@6.2.2':
resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==} resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==}
'@push.rocks/lik@6.3.1':
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
'@push.rocks/mongodump@1.1.0': '@push.rocks/mongodump@1.1.0':
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==} resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
@@ -981,8 +984,8 @@ packages:
'@push.rocks/smartmatch@2.0.0': '@push.rocks/smartmatch@2.0.0':
resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==} resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==}
'@push.rocks/smartmetrics@3.0.1': '@push.rocks/smartmetrics@3.0.2':
resolution: {integrity: sha512-wdc9v8F0dQXwraAVCZjGvt2lnGpbVYHWzER9OFR+u+ultMq7aLjUevxSpkS8BkhBMLTuNMCCWIqyAzaCqHzFgQ==} resolution: {integrity: sha512-bW60TrdCubsZwsYK1+lE9y+OoXN8MfB7bhSASlTKtwhExdCbjXYXnSt9W7zerD7HbsUNOsvejIjX9q4oju+P2g==}
'@push.rocks/smartmime@1.0.6': '@push.rocks/smartmime@1.0.6':
resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==} resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==}
@@ -996,8 +999,8 @@ packages:
'@push.rocks/smartmongo@5.1.0': '@push.rocks/smartmongo@5.1.0':
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==} resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
'@push.rocks/smartmta@5.3.0': '@push.rocks/smartmta@5.3.1':
resolution: {integrity: sha512-uJI25fslzvrcenU36WCdt5gB8cCfkjUlY7PqlxEtFp474/l/kZxNnvirv1gnZLRNNa+ioe5aH18HKE+KcAjuxA==} resolution: {integrity: sha512-cEuXO56i/zL9eZS79eAesEW16ikdBJKLlEv9pLKkt2cmaHBWADGHjeOzJmsszQ9CSFcuhd41aHYVGMZXVvsG2g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
cpu: [x64, arm64] cpu: [x64, arm64]
os: [darwin, linux, win32] os: [darwin, linux, win32]
@@ -1035,8 +1038,8 @@ packages:
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@25.8.5': '@push.rocks/smartproxy@25.9.1':
resolution: {integrity: sha512-oLmV+Bq7sSgQP9McTao/imb6Xb62QM7wlTFt5kNynrS5WK2wAe8cEjDKOcyu8N/WmzNCEClT5f/0xAtI6JxtkA==} resolution: {integrity: sha512-X9Yc74CwwvxUay/WDdomRL48vkQyb/kT/QZ4r+AyUUJV2tlt0rQxhbegv9C9kkjrVNsflQWjrAKseh0GB4hAfA==}
'@push.rocks/smartpuppeteer@2.0.5': '@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1083,8 +1086,8 @@ packages:
'@push.rocks/smartspawn@3.0.3': '@push.rocks/smartspawn@3.0.3':
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
'@push.rocks/smartstate@2.0.30': '@push.rocks/smartstate@2.2.0':
resolution: {integrity: sha512-IuNW8XtSumXIr7g7MIFyWg5PBwLF2mwsymTJbSEycK2Pa9ZLk4yjRHnR907xCilxgiMU9ixQZyNdpa5MMF999A==} resolution: {integrity: sha512-e41vA1y9b0HBauzjMSh3l0YlRhcG4jhArm43/HHNdT+inxEGIeRL24VGeq+sl2MUr/eFWqgrETXhvL3YrsYFaw==}
'@push.rocks/smartstream@2.0.8': '@push.rocks/smartstream@2.0.8':
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
@@ -1344,8 +1347,8 @@ packages:
'@serve.zone/interfaces@5.3.0': '@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@4.3.0': '@serve.zone/remoteingress@4.4.0':
resolution: {integrity: sha512-yk14uS6oWIP83Zpem4hGf8zi3W9pefnxijtSWp45WvZ+u9XTXIADQNaUZBSTCId8CYkfPkfRGaaaARunVdjFXg==} resolution: {integrity: sha512-+M2EHP0irezN0xutYn0H6ZXePWoMJy4ETbK9/5Zgb4nx2FbDRYQyFLJUXyLzVL/rtJ/5trToPwOa3ljZoVze3g==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -1835,11 +1838,11 @@ packages:
'@types/node@18.19.130': '@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@22.19.11': '@types/node@22.19.13':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==}
'@types/node@25.3.0': '@types/node@25.3.3':
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -2081,8 +2084,8 @@ packages:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
brace-expansion@5.0.3: brace-expansion@5.0.4:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
broadcast-channel@7.3.0: broadcast-channel@7.3.0:
@@ -3244,8 +3247,8 @@ packages:
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@10.2.2: minimatch@10.2.4:
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
minimatch@3.1.3: minimatch@3.1.3:
@@ -4291,11 +4294,11 @@ snapshots:
'@api.global/typedrequest-interfaces@3.0.19': {} '@api.global/typedrequest-interfaces@3.0.19': {}
'@api.global/typedrequest@3.2.6': '@api.global/typedrequest@3.2.7':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.3.1
'@push.rocks/smartbuffer': 3.0.5 '@push.rocks/smartbuffer': 3.0.5
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartguard': 3.1.0 '@push.rocks/smartguard': 3.1.0
@@ -4305,7 +4308,7 @@ snapshots:
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)': '@api.global/typedserver@3.0.80(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260210.0
@@ -4353,7 +4356,7 @@ snapshots:
'@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)': '@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260303.0 '@cloudflare/workers-types': 4.20260303.0
@@ -4399,7 +4402,7 @@ snapshots:
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)': '@api.global/typedsocket@3.1.1(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
@@ -4419,7 +4422,7 @@ snapshots:
'@api.global/typedsocket@4.1.2(@push.rocks/smartserve@2.0.1)': '@api.global/typedsocket@4.1.2(@push.rocks/smartserve@2.0.1)':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4994,14 +4997,14 @@ snapshots:
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
broadcast-channel: 7.3.0 broadcast-channel: 7.3.0
'@design.estate/dees-domtools@2.3.8': '@design.estate/dees-domtools@2.3.8':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -5010,7 +5013,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrouter': 1.3.3 '@push.rocks/smartrouter': 1.3.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstate': 2.0.30 '@push.rocks/smartstate': 2.2.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 3.0.37
@@ -5334,7 +5337,7 @@ snapshots:
'@inquirer/figures': 1.0.15 '@inquirer/figures': 1.0.15
'@inquirer/type': 2.0.0 '@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4 '@types/mute-stream': 0.0.4
'@types/node': 22.19.11 '@types/node': 22.19.13
'@types/wrap-ansi': 3.0.0 '@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
cli-width: 4.1.0 cli-width: 4.1.0
@@ -5676,7 +5679,7 @@ snapshots:
'@push.rocks/levelcache@3.2.0': '@push.rocks/levelcache@3.2.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.3.1
'@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartcache': 1.0.18 '@push.rocks/smartcache': 1.0.18
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
@@ -5707,6 +5710,17 @@ snapshots:
'@types/symbol-tree': 3.2.5 '@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4 symbol-tree: 3.2.4
'@push.rocks/lik@6.3.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3
'@types/minimatch': 5.1.2
'@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4
'@push.rocks/mongodump@1.1.0(socks@2.8.7)': '@push.rocks/mongodump@1.1.0(socks@2.8.7)':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
@@ -5751,7 +5765,7 @@ snapshots:
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@configvault.io/interfaces': 1.0.17 '@configvault.io/interfaces': 1.0.17
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -6165,7 +6179,7 @@ snapshots:
dependencies: dependencies:
matcher: 5.0.0 matcher: 5.0.0
'@push.rocks/smartmetrics@3.0.1': '@push.rocks/smartmetrics@3.0.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -6233,7 +6247,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartmta@5.3.0': '@push.rocks/smartmta@5.3.1':
dependencies: dependencies:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
@@ -6340,13 +6354,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.8.5': '@push.rocks/smartproxy@25.9.1':
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartrust': 1.3.1 '@push.rocks/smartrust': 1.3.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
minimatch: 10.2.2 minimatch: 10.2.4
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
@@ -6424,7 +6438,7 @@ snapshots:
'@push.rocks/smartserve@2.0.1': '@push.rocks/smartserve@2.0.1':
dependencies: dependencies:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.7
'@cfworker/json-schema': 4.1.1 '@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
@@ -6487,9 +6501,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartstate@2.0.30': '@push.rocks/smartstate@2.2.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartjson': 6.0.0 '@push.rocks/smartjson': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -6827,7 +6840,7 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@serve.zone/remoteingress@4.3.0': '@serve.zone/remoteingress@4.4.0':
dependencies: dependencies:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartrust': 1.3.1 '@push.rocks/smartrust': 1.3.1
@@ -7359,22 +7372,22 @@ snapshots:
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -7382,7 +7395,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 1.2.1 '@types/send': 1.2.1
@@ -7395,17 +7408,17 @@ snapshots:
'@types/from2@2.3.6': '@types/from2@2.3.6':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7427,12 +7440,12 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -7455,26 +7468,26 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
form-data: 4.0.5 form-data: 4.0.5
'@types/node-forge@1.3.14': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/node@18.19.130': '@types/node@18.19.130':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
'@types/node@22.19.11': '@types/node@22.19.13':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.3.0': '@types/node@25.3.3':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.18.2
@@ -7492,22 +7505,22 @@ snapshots:
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7537,11 +7550,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.3.0 '@types/node': 25.3.3
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -7718,7 +7731,7 @@ snapshots:
dependencies: dependencies:
balanced-match: 4.0.2 balanced-match: 4.0.2
brace-expansion@5.0.3: brace-expansion@5.0.4:
dependencies: dependencies:
balanced-match: 4.0.4 balanced-match: 4.0.4
@@ -8018,7 +8031,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.19 '@types/cors': 2.8.19
'@types/node': 25.3.0 '@types/node': 25.3.3
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@@ -9166,9 +9179,9 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.2 brace-expansion: 5.0.2
minimatch@10.2.2: minimatch@10.2.4:
dependencies: dependencies:
brace-expansion: 5.0.3 brace-expansion: 5.0.4
minimatch@3.1.3: minimatch@3.1.3:
dependencies: dependencies:

View File

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

View File

@@ -23,6 +23,7 @@ import { MetricsManager } from './monitoring/index.js';
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js'; import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js'; import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { RouteConfigManager, ApiTokenManager } from './config/index.js'; import { RouteConfigManager, ApiTokenManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
export interface IDcRouterOptions { export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -221,7 +222,8 @@ export class DcRouter {
public detectedPublicIp: string | null = null; public detectedPublicIp: string | null = null;
// DNS query logging rate limiter state // DNS query logging rate limiter state
private dnsLogWindow: number[] = []; private dnsLogWindowSecond: number = 0; // epoch second of current window
private dnsLogWindowCount: number = 0; // queries logged this second
private dnsBatchCount: number = 0; private dnsBatchCount: number = 0;
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null; private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
@@ -900,7 +902,8 @@ export class DcRouter {
} }
this.dnsBatchTimer = null; this.dnsBatchTimer = null;
this.dnsBatchCount = 0; this.dnsBatchCount = 0;
this.dnsLogWindow = []; this.dnsLogWindowSecond = 0;
this.dnsLogWindowCount = 0;
} }
await this.opsServer.stop(); await this.opsServer.stop();
@@ -956,6 +959,7 @@ export class DcRouter {
// Stop cache database after other services (they may need it during shutdown) // Stop cache database after other services (they may need it during shutdown)
if (this.cacheDb) { if (this.cacheDb) {
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) })); await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
CacheDb.resetInstance();
} }
// Clear backoff cache in cert scheduler // Clear backoff cache in cert scheduler
@@ -979,6 +983,11 @@ export class DcRouter {
this.apiTokenManager = undefined; this.apiTokenManager = undefined;
this.certificateStatusMap.clear(); this.certificateStatusMap.clear();
// Reset security singletons to allow GC
SecurityLogger.resetInstance();
ContentScanner.resetInstance();
IPReputationChecker.resetInstance();
logger.log('info', 'All DcRouter services stopped'); logger.log('info', 'All DcRouter services stopped');
} catch (error) { } catch (error) {
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) }); logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
@@ -1305,11 +1314,14 @@ export class DcRouter {
} }
// Adaptive logging: individual logs up to 2/sec, then batch // Adaptive logging: individual logs up to 2/sec, then batch
const now = Date.now(); const nowSec = Math.floor(Date.now() / 1000);
this.dnsLogWindow = this.dnsLogWindow.filter(t => now - t < 1000); if (nowSec !== this.dnsLogWindowSecond) {
this.dnsLogWindowSecond = nowSec;
this.dnsLogWindowCount = 0;
}
if (this.dnsLogWindow.length < 2) { if (this.dnsLogWindowCount < 2) {
this.dnsLogWindow.push(now); this.dnsLogWindowCount++;
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', '); const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' }); logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
} else { } else {
@@ -1363,15 +1375,25 @@ export class DcRouter {
return; return;
} }
// Prevent uncaught exception from socket 'error' events
socket.on('error', (err) => {
logger.log('error', `DNS socket error: ${err.message}`);
if (!socket.destroyed) {
socket.destroy();
}
});
logger.log('debug', 'DNS socket handler: passing socket to DnsServer'); logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
try { try {
// Use the built-in socket handler from smartdns // Use the built-in socket handler from smartdns
// This handles HTTP/2, DoH protocol, etc. // This handles HTTP/2, DoH protocol, etc.
await (this.dnsServer as any).handleHttpsSocket(socket); await (this.dnsServer as any).handleHttpsSocket(socket);
} catch (error) { } catch (error) {
logger.log('error', `DNS socket handler error: ${error.message}`); logger.log('error', `DNS socket handler error: ${error.message}`);
socket.destroy(); if (!socket.destroyed) {
socket.destroy();
}
} }
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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