Compare commits

...

2 Commits

Author SHA1 Message Date
5b473de354 v3.0.0
Some checks failed
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-02 00:36:19 +00:00
1a108fa8b7 BREAKING CHANGE(deps): upgrade major dependencies, migrate action.target to action.targets (array), adapt to SmartRequest API changes, and add RADIUS server support 2026-02-02 00:36:19 +00:00
26 changed files with 1809 additions and 583 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,5 +1,10 @@
# Changelog # Changelog
## 2026-02-01 - 3.0.0 - BREAKING CHANGE(deps)
upgrade major dependencies, migrate action.target to action.targets (array), adapt to SmartRequest API changes, and add RADIUS server support
- Bumped many major dependencies: @api.global/typedserver 3.x → 8.3.0, @api.global/typedsocket 3.x → 4.1.0, @apiclient.xyz/cloudflare 6.x → 7.1.0, @design.estate/dees-catalog 1.x → 3.41.4, @push.rocks/smartpath 5.x → 6.x, @push.rocks/smartproxy 19.x → 22.x, @push.rocks/smartrequest 2.x → 5.x, uuid 11.x → 13.x, @types/node 25.1.0 → 25.2.0
## 2026-02-01 - 2.13.0 - feat(radius) ## 2026-02-01 - 2.13.0 - feat(radius)
add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "2.13.0", "version": "3.0.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
@@ -22,16 +22,16 @@
"@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.0.1", "@git.zone/tswatch": "^3.0.1",
"@types/node": "^25.1.0", "@types/node": "^25.2.0",
"node-forge": "^1.3.3" "node-forge": "^1.3.3"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.0.19", "@api.global/typedrequest": "^3.0.19",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^3.0.74", "@api.global/typedserver": "^8.3.0",
"@api.global/typedsocket": "^3.0.0", "@api.global/typedsocket": "^4.1.0",
"@apiclient.xyz/cloudflare": "^6.4.1", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^1.10.10", "@design.estate/dees-catalog": "^3.41.5",
"@design.estate/dees-element": "^2.0.45", "@design.estate/dees-element": "^2.0.45",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
@@ -45,11 +45,11 @@
"@push.rocks/smartmail": "^2.2.0", "@push.rocks/smartmail": "^2.2.0",
"@push.rocks/smartmetrics": "^2.0.10", "@push.rocks/smartmetrics": "^2.0.10",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^5.0.5", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartproxy": "^19.6.15", "@push.rocks/smartproxy": "^22.4.2",
"@push.rocks/smartradius": "^1.0.3", "@push.rocks/smartradius": "^1.0.3",
"@push.rocks/smartrequest": "^2.1.0", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrule": "^2.0.1", "@push.rocks/smartrule": "^2.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.0.27", "@push.rocks/smartstate": "^2.0.27",
@@ -61,7 +61,7 @@
"lru-cache": "^11.2.5", "lru-cache": "^11.2.5",
"mailauth": "^4.12.0", "mailauth": "^4.12.0",
"mailparser": "^3.9.3", "mailparser": "^3.9.3",
"uuid": "^11.1.0" "uuid": "^13.0.0"
}, },
"keywords": [ "keywords": [
"mail service", "mail service",

219
pnpm-lock.yaml generated
View File

@@ -15,17 +15,17 @@ importers:
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
'@api.global/typedserver': '@api.global/typedserver':
specifier: ^3.0.74 specifier: ^8.3.0
version: 3.0.80(@push.rocks/smartserve@2.0.1) version: 8.3.0(@tiptap/pm@2.27.2)
'@api.global/typedsocket': '@api.global/typedsocket':
specifier: ^3.0.0 specifier: ^4.1.0
version: 3.1.1(@push.rocks/smartserve@2.0.1) version: 4.1.0(@push.rocks/smartserve@2.0.1)
'@apiclient.xyz/cloudflare': '@apiclient.xyz/cloudflare':
specifier: ^6.4.1 specifier: ^7.1.0
version: 6.4.3 version: 7.1.0
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^1.10.10 specifier: ^3.41.5
version: 1.12.4(@tiptap/pm@2.27.2) version: 3.41.5(@tiptap/pm@2.27.2)
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.0.45 specifier: ^2.0.45
version: 2.1.6 version: 2.1.6
@@ -37,7 +37,7 @@ importers:
version: 6.1.3 version: 6.1.3
'@push.rocks/smartacme': '@push.rocks/smartacme':
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7) version: 8.0.0(socks@2.8.7)
'@push.rocks/smartdata': '@push.rocks/smartdata':
specifier: ^5.15.1 specifier: ^5.15.1
version: 5.16.7(socks@2.8.7) version: 5.16.7(socks@2.8.7)
@@ -66,20 +66,20 @@ importers:
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
'@push.rocks/smartpath': '@push.rocks/smartpath':
specifier: ^5.0.5 specifier: ^6.0.0
version: 5.1.0 version: 6.0.0
'@push.rocks/smartpromise': '@push.rocks/smartpromise':
specifier: ^4.0.3 specifier: ^4.0.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^19.6.15 specifier: ^22.4.2
version: 19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7) version: 22.4.2(socks@2.8.7)
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.1.0 version: 1.1.0
'@push.rocks/smartrequest': '@push.rocks/smartrequest':
specifier: ^2.1.0 specifier: ^5.0.1
version: 2.1.0 version: 5.0.1
'@push.rocks/smartrule': '@push.rocks/smartrule':
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
@@ -114,8 +114,8 @@ importers:
specifier: ^3.9.3 specifier: ^3.9.3
version: 3.9.3 version: 3.9.3
uuid: uuid:
specifier: ^11.1.0 specifier: ^13.0.0
version: 11.1.0 version: 13.0.0
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^4.1.2 specifier: ^4.1.2
@@ -133,8 +133,8 @@ importers:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1(@tiptap/pm@2.27.2) version: 3.0.1(@tiptap/pm@2.27.2)
'@types/node': '@types/node':
specifier: ^25.1.0 specifier: ^25.2.0
version: 25.1.0 version: 25.2.0
node-forge: node-forge:
specifier: ^1.3.3 specifier: ^1.3.3
version: 1.3.3 version: 1.3.3
@@ -172,6 +172,9 @@ packages:
'@apiclient.xyz/cloudflare@6.4.3': '@apiclient.xyz/cloudflare@6.4.3':
resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==} resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
'@apiclient.xyz/cloudflare@7.1.0':
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
'@aws-crypto/crc32@5.2.0': '@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@@ -359,11 +362,8 @@ packages:
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@1.12.4': '@design.estate/dees-catalog@3.41.5':
resolution: {integrity: sha512-tzNW3b1BQkbE7W2DcwFqXHy+igHzxMHoR+awCAE4/EzHl8kOJc4jF1RNyCe34LsuNaELgZ95Hde/HGgEC8JasA==} resolution: {integrity: sha512-2LOUh92h2ndzlEKOyDqGE2Mdjhmxt6ZeAqHt5KKslNHzmNhdWFKUe6C1Vm2nU6vRvFLXXC/ex56KSP3XcCPD8g==}
'@design.estate/dees-catalog@3.41.4':
resolution: {integrity: sha512-tVut61OuMF+o/1dzcKEvfom6sl8dMC5WzxIevN9jVq4sQgMO9DOrFd+UfKwRL9FfLZeqAFyxJDzU8389e4Pg0Q==}
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -374,9 +374,6 @@ packages:
'@design.estate/dees-element@2.1.6': '@design.estate/dees-element@2.1.6':
resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==} resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==}
'@design.estate/dees-wcctools@1.3.0':
resolution: {integrity: sha512-+yd8c1gTIKNRQYCvG0xu6Am8dHsRm7ymluX2gnoBQN4aFOpZgIBi/v9CvGyPhTD1p/VRouIBz1wsUCejnwrFCA==}
'@design.estate/dees-wcctools@3.8.0': '@design.estate/dees-wcctools@3.8.0':
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==} resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
@@ -1066,8 +1063,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@19.6.17': '@push.rocks/smartproxy@22.4.2':
resolution: {integrity: sha512-5y6lVxlHXoVQXAQLr5S+2ifxZf9EID32twyeuZTS9tDyof0wJKppLzKQepwB7hfQXS2J06JBN7oa9n0mguELBg==} resolution: {integrity: sha512-JdDa1VxGOnWfF5HuJRvkX3/zHuIKz+IV9n/XOsNZQA9zMZdLVlWPqjGio9GLWsPOWA2l1YZKymjMH4ybPbGQtA==}
'@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==}
@@ -1876,6 +1873,10 @@ packages:
'@types/minimatch@5.1.2': '@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
'@types/minimatch@6.0.0':
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@@ -1894,8 +1895,8 @@ packages:
'@types/node@22.19.7': '@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
'@types/node@25.1.0': '@types/node@25.2.0':
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
'@types/pidusage@2.0.5': '@types/pidusage@2.0.5':
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==} resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
@@ -1969,9 +1970,6 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@webcontainer/api@1.2.0':
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
'@yr/monotone-cubic-spline@1.0.3': '@yr/monotone-cubic-spline@1.0.3':
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
@@ -3097,9 +3095,6 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'} engines: {node: '>=12'}
lucide@0.544.0:
resolution: {integrity: sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==}
lucide@0.563.0: lucide@0.563.0:
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==} resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
@@ -3343,9 +3338,6 @@ packages:
mitt@3.0.1: mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
monaco-editor@0.52.2:
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
monaco-editor@0.55.1: monaco-editor@0.55.1:
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
@@ -4206,8 +4198,8 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
uuid@11.1.0: uuid@13.0.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true hasBin: true
uuid@9.0.1: uuid@9.0.1:
@@ -4438,7 +4430,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260131.0 '@cloudflare/workers-types': 4.20260131.0
'@design.estate/dees-catalog': 3.41.4(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.41.5(@tiptap/pm@2.27.2)
'@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
@@ -4485,7 +4477,7 @@ snapshots:
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartsocket': 2.1.0(@push.rocks/smartserve@2.0.1) '@push.rocks/smartsocket': 2.1.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
optionalDependencies: optionalDependencies:
@@ -4523,6 +4515,18 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
'@apiclient.xyz/cloudflare@7.1.0':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartstring': 4.1.0
'@tsclass/tsclass': 9.3.0
cloudflare: 5.2.0
transitivePeerDependencies:
- encoding
'@aws-crypto/crc32@5.2.0': '@aws-crypto/crc32@5.2.0':
dependencies: dependencies:
'@aws-crypto/util': 5.2.0 '@aws-crypto/util': 5.2.0
@@ -5028,43 +5032,7 @@ snapshots:
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@1.12.4(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.41.5(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6
'@design.estate/dees-wcctools': 1.3.0
'@fortawesome/fontawesome-svg-core': 7.1.0
'@fortawesome/free-brands-svg-icons': 7.1.0
'@fortawesome/free-regular-svg-icons': 7.1.0
'@fortawesome/free-solid-svg-icons': 7.1.0
'@push.rocks/smarti18n': 1.0.4
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
'@tiptap/extension-link': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
'@tiptap/extension-text-align': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/extension-typography': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/starter-kit': 2.27.2
'@tsclass/tsclass': 9.3.0
'@webcontainer/api': 1.2.0
apexcharts: 5.3.6
highlight.js: 11.11.1
ibantools: 4.5.1
lit: 3.3.2
lucide: 0.544.0
monaco-editor: 0.52.2
pdfjs-dist: 4.10.38
xterm: 5.3.0
xterm-addon-fit: 0.8.0(xterm@5.3.0)
transitivePeerDependencies:
- '@nuxt/kit'
- '@tiptap/pm'
- react
- supports-color
- vue
'@design.estate/dees-catalog@3.41.4(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.8 '@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
@@ -5144,18 +5112,6 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@design.estate/dees-wcctools@1.3.0':
dependencies:
'@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@design.estate/dees-wcctools@3.8.0': '@design.estate/dees-wcctools@3.8.0':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.8 '@design.estate/dees-domtools': 2.3.8
@@ -5920,7 +5876,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)': '@push.rocks/smartacme@8.0.0(socks@2.8.7)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1) '@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
'@apiclient.xyz/cloudflare': 6.4.3 '@apiclient.xyz/cloudflare': 6.4.3
@@ -5942,7 +5898,6 @@ snapshots:
- '@aws-sdk/credential-providers' - '@aws-sdk/credential-providers'
- '@mongodb-js/zstd' - '@mongodb-js/zstd'
- '@nuxt/kit' - '@nuxt/kit'
- '@push.rocks/smartserve'
- bare-abort-controller - bare-abort-controller
- encoding - encoding
- gcp-metadata - gcp-metadata
@@ -6454,22 +6409,22 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)': '@push.rocks/smartproxy@22.4.2(socks@2.8.7)':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7) '@push.rocks/smartacme': 8.0.0(socks@2.8.7)
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog': 3.1.10
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/taskbuffer': 3.5.0 '@push.rocks/taskbuffer': 3.5.0
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@types/minimatch': 5.1.2 '@types/minimatch': 6.0.0
'@types/ws': 8.18.1 '@types/ws': 8.18.1
minimatch: 10.1.1 minimatch: 10.1.1
pretty-ms: 9.3.0 pretty-ms: 9.3.0
@@ -6478,7 +6433,6 @@ snapshots:
- '@aws-sdk/credential-providers' - '@aws-sdk/credential-providers'
- '@mongodb-js/zstd' - '@mongodb-js/zstd'
- '@nuxt/kit' - '@nuxt/kit'
- '@push.rocks/smartserve'
- bare-abort-controller - bare-abort-controller
- bufferutil - bufferutil
- encoding - encoding
@@ -6592,7 +6546,7 @@ snapshots:
'@push.rocks/webrequest': 4.0.1 '@push.rocks/webrequest': 4.0.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@push.rocks/smartsocket@2.1.0(@push.rocks/smartserve@2.0.1)': '@push.rocks/smartsocket@2.1.0':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1) '@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
@@ -6611,7 +6565,6 @@ snapshots:
socket.io-client: 4.8.1 socket.io-client: 4.8.1
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- '@push.rocks/smartserve'
- bufferutil - bufferutil
- react - react
- supports-color - supports-color
@@ -7462,27 +7415,27 @@ snapshots:
'@types/bn.js@5.2.0': '@types/bn.js@5.2.0':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
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.1.0 '@types/node': 25.2.0
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -7490,7 +7443,7 @@ snapshots:
'@types/dns-packet@5.6.5': '@types/dns-packet@5.6.5':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@types/elliptic@6.4.18': '@types/elliptic@6.4.18':
dependencies: dependencies:
@@ -7498,7 +7451,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@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
@@ -7511,17 +7464,17 @@ snapshots:
'@types/from2@2.3.6': '@types/from2@2.3.6':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7543,18 +7496,18 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
'@types/mailparser@3.4.6': '@types/mailparser@3.4.6':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
iconv-lite: 0.6.3 iconv-lite: 0.6.3
'@types/markdown-it@14.1.2': '@types/markdown-it@14.1.2':
@@ -7572,20 +7525,24 @@ snapshots:
'@types/minimatch@5.1.2': {} '@types/minimatch@5.1.2': {}
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.1.1
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
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.1.0 '@types/node': 25.2.0
'@types/node@18.19.130': '@types/node@18.19.130':
dependencies: dependencies:
@@ -7595,7 +7552,7 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.1.0': '@types/node@25.2.0':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@@ -7615,22 +7572,22 @@ snapshots:
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@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.1.0 '@types/node': 25.2.0
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7656,17 +7613,15 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.1.0 '@types/node': 25.2.0
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@webcontainer/api@1.2.0': {}
'@yr/monotone-cubic-spline@1.0.3': {} '@yr/monotone-cubic-spline@1.0.3': {}
'@zone-eu/mailsplit@5.4.8': '@zone-eu/mailsplit@5.4.8':
@@ -8146,7 +8101,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.1.0 '@types/node': 25.2.0
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@@ -8918,8 +8873,6 @@ snapshots:
lru-cache@7.18.3: {} lru-cache@7.18.3: {}
lucide@0.544.0: {}
lucide@0.563.0: {} lucide@0.563.0: {}
mailauth@4.12.0: mailauth@4.12.0:
@@ -9352,8 +9305,6 @@ snapshots:
mitt@3.0.1: {} mitt@3.0.1: {}
monaco-editor@0.52.2: {}
monaco-editor@0.55.1: monaco-editor@0.55.1:
dependencies: dependencies:
dompurify: 3.2.7 dompurify: 3.2.7
@@ -10372,7 +10323,7 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@11.1.0: {} uuid@13.0.0: {}
uuid@9.0.1: {} uuid@9.0.1: {}

View File

@@ -1,5 +1,71 @@
# Implementation Hints and Learnings # Implementation Hints and Learnings
## Dependency Upgrade (2026-02-01)
### Major Upgrades Completed
- `@api.global/typedserver`: 3.0.80 → 8.3.0
- `@api.global/typedsocket`: 3.1.1 → 4.1.0
- `@apiclient.xyz/cloudflare`: 6.4.3 → 7.1.0
- `@design.estate/dees-catalog`: 1.12.4 → 3.41.4
- `@push.rocks/smartpath`: 5.1.0 → 6.0.0
- `@push.rocks/smartproxy`: 19.6.17 → 22.4.2
- `@push.rocks/smartrequest`: 2.1.0 → 5.0.1
- `uuid`: 11.1.0 → 13.0.0
### Breaking Changes Fixed
1. **SmartProxy v22**: `target``targets` (array)
```typescript
// Old
action: { type: 'forward', target: { host: 'x', port: 25 } }
// New
action: { type: 'forward', targets: [{ host: 'x', port: 25 }] }
```
2. **SmartRequest v5**: `SmartRequestClient` → `SmartRequest`, `.body` → `.json()`
```typescript
// Old
const resp = await plugins.smartrequest.SmartRequestClient.create()...post();
const json = resp.body;
// New
const resp = await plugins.smartrequest.SmartRequest.create()...post();
const json = await resp.json();
```
3. **dees-catalog v3**: Icon naming changed to library-prefixed format
```typescript
// Old (deprecated but supported)
<dees-icon iconFA="check"></dees-icon>
// New
<dees-icon icon="fa:check"></dees-icon>
<dees-icon icon="lucide:menu"></dees-icon>
```
### TC39 Decorators
- ts_web components updated to use `accessor` keyword for `@state()` decorators
- Required for TC39 standard decorator support
### tswatch Configuration
The project now uses tswatch for development:
```bash
pnpm run watch
```
Configuration in `npmextra.json`:
```json
{
"@git.zone/tswatch": {
"watchers": [{
"name": "dcrouter-dev",
"watch": ["ts/**/*.ts", "ts_*/**/*.ts", "test_watch/devserver.ts"],
"command": "pnpm run build && tsrun test_watch/devserver.ts",
"restart": true,
"debounce": 500,
"runOnStart": true
}]
}
}
```
## RADIUS Server Integration (2026-02-01) ## RADIUS Server Integration (2026-02-01)
### Overview ### Overview
@@ -1131,3 +1197,71 @@ The throughput was showing 0 because:
4. Updated frontend to call the new endpoint for complete network metrics 4. Updated frontend to call the new endpoint for complete network metrics
The throughput data now flows correctly from SmartProxy → MetricsManager → API → UI. The throughput data now flows correctly from SmartProxy → MetricsManager → API → UI.
## Email Operations Dashboard (2026-02-01)
### Overview
Replaced mock data in the email UI with real backend data from the delivery queue and security logger.
### New Files Created
- `ts_interfaces/requests/email-ops.ts` - TypedRequest interfaces for email operations
- `ts/opsserver/handlers/email-ops.handler.ts` - Backend handler for email operations
### Key Interfaces
- `IReq_GetQueuedEmails` - Fetch emails from delivery queue by status
- `IReq_GetSentEmails` - Fetch delivered emails
- `IReq_GetFailedEmails` - Fetch failed emails
- `IReq_ResendEmail` - Re-queue a failed email for retry
- `IReq_GetSecurityIncidents` - Fetch security events from SecurityLogger
- `IReq_GetBounceRecords` - Fetch bounce records and suppression list
- `IReq_RemoveFromSuppressionList` - Remove email from suppression list
### UI Changes (ops-view-emails.ts)
- Replaced mock folders (inbox/sent/draft/trash) with operations views:
- **Queued**: Emails pending delivery
- **Sent**: Successfully delivered emails
- **Failed**: Failed emails with resend capability
- **Security**: Security incidents from SecurityLogger
- Removed `generateMockEmails()` method
- Added state management via `emailOpsStatePart` in appstate.ts
- Added resend button for failed emails
- Added security incident detail view
### Data Flow
```
UnifiedDeliveryQueue → EmailOpsHandler → TypedRequest → Frontend State → UI
SecurityLogger → EmailOpsHandler → TypedRequest → Frontend State → UI
BounceManager → EmailOpsHandler → TypedRequest → Frontend State → UI
```
### Backend Data Access
The handler accesses data from:
- `dcRouter.emailServer.deliveryQueue` - Email queue items (IQueueItem)
- `SecurityLogger.getInstance()` - Security events (ISecurityEvent)
- `emailServer.bounceManager` - Bounce records and suppression list
## OpsServer UI Fixes (2026-02-02)
### Configuration Page Fix
The configuration page had field name mismatches between frontend and backend:
- Frontend expected `server` and `storage` sections
- Backend returns `proxy` section (not `server`)
- Backend has no `storage` section
**Fix**: Updated `ops-view-config.ts` to use correct section names:
- `proxy` instead of `server`
- Removed non-existent `storage` section
- Added optional chaining (`?.`) for safety
### Auth Persistence Fix
Login state was using `'soft'` mode in Smartstate which is memory-only:
- User login was lost on page refresh
- State reset to logged out after browser restart
**Changes**:
1. `ts_web/appstate.ts`: Changed loginStatePart from `'soft'` to `'persistent'`
- Now uses IndexedDB to persist across browser sessions
2. `ts/opsserver/handlers/admin.handler.ts`: JWT expiry changed from 7 days to 24 hours
3. `ts_web/elements/ops-dashboard.ts`: Added JWT expiry check on session restore
- Validates stored JWT hasn't expired before auto-logging in
- Clears expired sessions and shows login form

View File

@@ -40,6 +40,11 @@ A comprehensive traffic routing solution that provides unified gateway capabilit
- **DKIM, SPF, DMARC** authentication and verification - **DKIM, SPF, DMARC** authentication and verification
- **Enterprise deliverability** with IP warmup and reputation management - **Enterprise deliverability** with IP warmup and reputation management
### 📡 **RADIUS Server**
- **MAC Authentication Bypass (MAB)** for network device authentication
- **VLAN assignment** based on MAC address or OUI patterns
- **RADIUS accounting** for session tracking and billing
### ⚡ **High Performance** ### ⚡ **High Performance**
- **Connection pooling** and efficient resource management - **Connection pooling** and efficient resource management
- **Load balancing** with automatic failover - **Load balancing** with automatic failover
@@ -79,7 +84,7 @@ const router = new DcRouter({
match: { domains: ['example.com'], ports: [443] }, match: { domains: ['example.com'], ports: [443] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: '192.168.1.10', port: 8080 }, targets: [{ host: '192.168.1.10', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' } tls: { mode: 'terminate', certificate: 'auto' }
} }
} }
@@ -271,10 +276,10 @@ interface IRouteConfig {
}; };
action: { action: {
type: 'forward' | 'redirect' | 'serve'; type: 'forward' | 'redirect' | 'serve';
target?: { targets?: Array<{
host: string; host: string;
port: number | 'preserve' | ((context: any) => number); port: number | 'preserve' | ((context: any) => number);
}; }>;
tls?: { tls?: {
mode: 'terminate' | 'passthrough'; mode: 'terminate' | 'passthrough';
certificate?: 'auto' | string; certificate?: 'auto' | string;
@@ -640,15 +645,7 @@ const routes = [
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{ host: '192.168.1.20', port: 8080 }],
host: '192.168.1.20',
port: (context) => {
// Route based on path
if (context.path.startsWith('/v1/')) return 8080;
if (context.path.startsWith('/v2/')) return 8081;
return 8080;
}
},
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -687,10 +684,7 @@ const tcpRoutes = [
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{ host: '192.168.1.30', port: 'preserve' }],
host: '192.168.1.30',
port: 'preserve'
},
security: { security: {
ipAllowList: ['192.168.1.0/24'] ipAllowList: ['192.168.1.0/24']
} }
@@ -706,10 +700,7 @@ const tcpRoutes = [
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{ host: '192.168.1.40', port: 8443 }],
host: '192.168.1.40',
port: 8443
},
tls: { tls: {
mode: 'passthrough' mode: 'passthrough'
} }
@@ -893,7 +884,7 @@ const router = new DcRouter({
match: { domains: ['example.com', 'www.example.com'], ports: [443] }, match: { domains: ['example.com', 'www.example.com'], ports: [443] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: '192.168.1.10', port: 80 }, targets: [{ host: '192.168.1.10', port: 80 }],
tls: { mode: 'terminate', certificate: 'auto' } tls: { mode: 'terminate', certificate: 'auto' }
} }
}, },
@@ -905,7 +896,7 @@ const router = new DcRouter({
match: { domains: ['api.example.com'], ports: [443] }, match: { domains: ['api.example.com'], ports: [443] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: '192.168.1.20', port: 8080 }, targets: [{ host: '192.168.1.20', port: 8080 }],
tls: { mode: 'terminate', certificate: 'auto' } tls: { mode: 'terminate', certificate: 'auto' }
} }
}, },
@@ -916,7 +907,7 @@ const router = new DcRouter({
match: { ports: [{ from: 8000, to: 8999 }] }, match: { ports: [{ from: 8000, to: 8999 }] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: '192.168.1.30', port: 'preserve' }, targets: [{ host: '192.168.1.30', port: 'preserve' }],
security: { ipAllowList: ['192.168.0.0/16'] } security: { ipAllowList: ['192.168.0.0/16'] }
} }
} }

View File

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

View File

@@ -517,10 +517,10 @@ export class DcRouter {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: route.action.type === 'forward' && route.action.forward ? { targets: route.action.type === 'forward' && route.action.forward ? [{
host: route.action.forward.host, host: route.action.forward.host,
port: route.action.forward.port || 25 port: route.action.forward.port || 25
} : undefined, }] : undefined,
tls: { tls: {
mode: 'passthrough' mode: 'passthrough'
} }

View File

@@ -17,6 +17,7 @@ export class OpsServer {
private securityHandler: handlers.SecurityHandler; private securityHandler: handlers.SecurityHandler;
private statsHandler: handlers.StatsHandler; private statsHandler: handlers.StatsHandler;
private radiusHandler: handlers.RadiusHandler; private radiusHandler: handlers.RadiusHandler;
private emailOpsHandler: handlers.EmailOpsHandler;
constructor(dcRouterRefArg: DcRouter) { constructor(dcRouterRefArg: DcRouter) {
this.dcRouterRef = dcRouterRefArg; this.dcRouterRef = dcRouterRefArg;
@@ -55,6 +56,7 @@ export class OpsServer {
this.securityHandler = new handlers.SecurityHandler(this); this.securityHandler = new handlers.SecurityHandler(this);
this.statsHandler = new handlers.StatsHandler(this); this.statsHandler = new handlers.StatsHandler(this);
this.radiusHandler = new handlers.RadiusHandler(this); this.radiusHandler = new handlers.RadiusHandler(this);
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
console.log('✅ OpsServer TypedRequest handlers initialized'); console.log('✅ OpsServer TypedRequest handlers initialized');
} }

View File

@@ -73,7 +73,7 @@ export class AdminHandler {
throw new plugins.typedrequest.TypedResponseError('login failed'); throw new plugins.typedrequest.TypedResponseError('login failed');
} }
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24 * 7; // 7 days const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
const jwt = await this.smartjwtInstance.createJWT({ const jwt = await this.smartjwtInstance.createJWT({
userId: user.id, userId: user.id,

View File

@@ -0,0 +1,325 @@
import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { SecurityLogger } from '../../security/index.js';
export class EmailOpsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Queued Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueuedEmails>(
'getQueuedEmails',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return { items: [], total: 0 };
}
const queue = emailServer.deliveryQueue;
const stats = queue.getStats();
// Get all queue items and filter by status if provided
const items = this.getQueueItems(
dataArg.status,
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: stats.queueSize,
};
}
)
);
// Get Sent Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSentEmails>(
'getSentEmails',
async (dataArg) => {
const items = this.getQueueItems(
'delivered',
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: items.length, // Note: total would ideally come from a counter
};
}
)
);
// Get Failed Emails Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetFailedEmails>(
'getFailedEmails',
async (dataArg) => {
const items = this.getQueueItems(
'failed',
dataArg.limit || 50,
dataArg.offset || 0
);
return {
items,
total: items.length,
};
}
)
);
// Resend Failed Email Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
'resendEmail',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return { success: false, error: 'Email server not available' };
}
const queue = emailServer.deliveryQueue;
const item = queue.getItem(dataArg.emailId);
if (!item) {
return { success: false, error: 'Email not found in queue' };
}
if (item.status !== 'failed') {
return { success: false, error: `Email is not in failed state (current: ${item.status})` };
}
try {
// Re-enqueue the failed email by creating a new queue entry
// with the same data but reset attempt count
const newQueueId = await queue.enqueue(
item.processingResult,
item.processingMode,
item.route
);
// Optionally remove the old failed entry
await queue.removeItem(dataArg.emailId);
return { success: true, newQueueId };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to resend email'
};
}
}
)
);
// Get Security Incidents Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityIncidents>(
'getSecurityIncidents',
async (dataArg) => {
const securityLogger = SecurityLogger.getInstance();
const filter: {
level?: any;
type?: any;
} = {};
if (dataArg.level) {
filter.level = dataArg.level;
}
if (dataArg.type) {
filter.type = dataArg.type;
}
const incidents = securityLogger.getRecentEvents(
dataArg.limit || 100,
Object.keys(filter).length > 0 ? filter : undefined
);
return {
incidents: incidents.map(event => ({
timestamp: event.timestamp,
level: event.level as interfaces.requests.TSecurityLogLevel,
type: event.type as interfaces.requests.TSecurityEventType,
message: event.message,
details: event.details,
ipAddress: event.ipAddress,
userId: event.userId,
sessionId: event.sessionId,
emailId: event.emailId,
domain: event.domain,
action: event.action,
result: event.result,
success: event.success,
})),
total: incidents.length,
};
}
)
);
// Get Bounce Records Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBounceRecords>(
'getBounceRecords',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
// Get bounce manager from email server via reflection
// BounceManager is private but we need to access it
const bounceManager = (emailServer as any)?.bounceManager;
if (!bounceManager) {
return { records: [], suppressionList: [], total: 0 };
}
// Get suppression list
const suppressionList = bounceManager.getSuppressionList();
// Get hard bounced addresses and convert to records
const hardBouncedAddresses = bounceManager.getHardBouncedAddresses();
// Create bounce records from the available data
const records: interfaces.requests.IBounceRecord[] = [];
for (const email of hardBouncedAddresses) {
const bounceInfo = bounceManager.getBounceInfo(email);
if (bounceInfo) {
records.push({
id: `bounce-${email}`,
recipient: email,
sender: '',
domain: email.split('@')[1] || '',
bounceType: bounceInfo.type as interfaces.requests.TBounceType,
bounceCategory: bounceInfo.category as interfaces.requests.TBounceCategory,
timestamp: bounceInfo.lastBounce,
processed: true,
});
}
}
// Apply limit and offset
const limit = dataArg.limit || 50;
const offset = dataArg.offset || 0;
const paginatedRecords = records.slice(offset, offset + limit);
return {
records: paginatedRecords,
suppressionList,
total: records.length,
};
}
)
);
// Remove from Suppression List Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveFromSuppressionList>(
'removeFromSuppressionList',
async (dataArg) => {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
const bounceManager = (emailServer as any)?.bounceManager;
if (!bounceManager) {
return { success: false, error: 'Bounce manager not available' };
}
try {
bounceManager.removeFromSuppressionList(dataArg.email);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to remove from suppression list'
};
}
}
)
);
}
/**
* Helper method to get queue items with filtering and pagination
*/
private getQueueItems(
status?: interfaces.requests.TEmailQueueStatus,
limit: number = 50,
offset: number = 0
): interfaces.requests.IEmailQueueItem[] {
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
if (!emailServer?.deliveryQueue) {
return [];
}
const queue = emailServer.deliveryQueue;
const items: interfaces.requests.IEmailQueueItem[] = [];
// Access the internal queue map via reflection
// This is necessary because the queue doesn't expose iteration methods
const queueMap = (queue as any).queue as Map<string, any>;
if (!queueMap) {
return [];
}
// Filter and convert items
for (const [id, item] of queueMap.entries()) {
// Apply status filter if provided
if (status && item.status !== status) {
continue;
}
// Extract email details from processingResult if available
const processingResult = item.processingResult;
let from = '';
let to: string[] = [];
let subject = '';
if (processingResult) {
// Check if it's an Email object or raw email data
if (processingResult.email) {
from = processingResult.email.from || '';
to = processingResult.email.to || [];
subject = processingResult.email.subject || '';
} else if (processingResult.from) {
from = processingResult.from;
to = processingResult.to || [];
subject = processingResult.subject || '';
}
}
items.push({
id: item.id,
processingMode: item.processingMode,
status: item.status,
attempts: item.attempts,
nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt,
lastError: item.lastError,
createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt,
updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt,
deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt,
from,
to,
subject,
});
}
// Sort by createdAt descending (newest first)
items.sort((a, b) => b.createdAt - a.createdAt);
// Apply pagination
return items.slice(offset, offset + limit);
}
}

View File

@@ -4,3 +4,4 @@ export * from './logs.handler.js';
export * from './security.handler.js'; export * from './security.handler.js';
export * from './stats.handler.js'; export * from './stats.handler.js';
export * from './radius.handler.js'; export * from './radius.handler.js';
export * from './email-ops.handler.js';

View File

@@ -68,13 +68,13 @@ export class SmsService {
recipients: [{ msisdn: toNumber }], recipients: [{ msisdn: toNumber }],
}; };
const resp = await plugins.smartrequest.SmartRequestClient.create() const resp = await plugins.smartrequest.SmartRequest.create()
.url('https://gatewayapi.com/rest/mtsms') .url('https://gatewayapi.com/rest/mtsms')
.header('Authorization', `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`) .header('Authorization', `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`)
.header('Content-Type', 'application/json') .header('Content-Type', 'application/json')
.json(payload) .json(payload)
.post(); .post();
const json = resp.body; const json = await resp.json();
logger.log('info', `sent an sms to ${toNumber} with text '${messageText}'`, { logger.log('info', `sent an sms to ${toNumber} with text '${messageText}'`, {
eventType: 'sentSms', eventType: 'sentSms',
sms: { sms: {

View File

@@ -0,0 +1,239 @@
import * as plugins from '../plugins.js';
import * as authInterfaces from '../data/auth.js';
// ============================================================================
// Email Queue Item Interface (matches backend IQueueItem)
// ============================================================================
export type TEmailQueueStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
export interface IEmailQueueItem {
id: string;
processingMode: 'forward' | 'mta' | 'process';
status: TEmailQueueStatus;
attempts: number;
nextAttempt: number; // timestamp
lastError?: string;
createdAt: number; // timestamp
updatedAt: number; // timestamp
deliveredAt?: number; // timestamp
// Email details extracted from processingResult
from?: string;
to?: string[];
subject?: string;
}
// ============================================================================
// Bounce Record Interface (matches backend BounceRecord)
// ============================================================================
export type TBounceType =
| 'invalid_recipient'
| 'domain_not_found'
| 'mailbox_full'
| 'mailbox_inactive'
| 'blocked'
| 'spam_related'
| 'policy_related'
| 'server_unavailable'
| 'temporary_failure'
| 'quota_exceeded'
| 'network_error'
| 'timeout'
| 'auto_response'
| 'challenge_response'
| 'unknown';
export type TBounceCategory = 'hard' | 'soft' | 'auto_response' | 'unknown';
export interface IBounceRecord {
id: string;
originalEmailId?: string;
recipient: string;
sender: string;
domain: string;
subject?: string;
bounceType: TBounceType;
bounceCategory: TBounceCategory;
timestamp: number;
smtpResponse?: string;
diagnosticCode?: string;
statusCode?: string;
processed: boolean;
retryCount?: number;
nextRetryTime?: number;
}
// ============================================================================
// Security Incident Interface (matches backend ISecurityEvent)
// ============================================================================
export type TSecurityLogLevel = 'info' | 'warn' | 'error' | 'critical';
export type TSecurityEventType =
| 'authentication'
| 'access_control'
| 'email_validation'
| 'email_processing'
| 'email_forwarding'
| 'email_delivery'
| 'dkim'
| 'spf'
| 'dmarc'
| 'rate_limit'
| 'rate_limiting'
| 'spam'
| 'malware'
| 'connection'
| 'data_exposure'
| 'configuration'
| 'ip_reputation'
| 'rejected_connection';
export interface ISecurityIncident {
timestamp: number;
level: TSecurityLogLevel;
type: TSecurityEventType;
message: string;
details?: any;
ipAddress?: string;
userId?: string;
sessionId?: string;
emailId?: string;
domain?: string;
action?: string;
result?: string;
success?: boolean;
}
// ============================================================================
// Get Queued Emails Request
// ============================================================================
export interface IReq_GetQueuedEmails extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetQueuedEmails
> {
method: 'getQueuedEmails';
request: {
identity?: authInterfaces.IIdentity;
status?: TEmailQueueStatus;
limit?: number;
offset?: number;
};
response: {
items: IEmailQueueItem[];
total: number;
};
}
// ============================================================================
// Get Sent Emails Request
// ============================================================================
export interface IReq_GetSentEmails extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetSentEmails
> {
method: 'getSentEmails';
request: {
identity?: authInterfaces.IIdentity;
limit?: number;
offset?: number;
};
response: {
items: IEmailQueueItem[];
total: number;
};
}
// ============================================================================
// Get Failed Emails Request
// ============================================================================
export interface IReq_GetFailedEmails extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetFailedEmails
> {
method: 'getFailedEmails';
request: {
identity?: authInterfaces.IIdentity;
limit?: number;
offset?: number;
};
response: {
items: IEmailQueueItem[];
total: number;
};
}
// ============================================================================
// Resend Failed Email Request
// ============================================================================
export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_ResendEmail
> {
method: 'resendEmail';
request: {
identity?: authInterfaces.IIdentity;
emailId: string;
};
response: {
success: boolean;
newQueueId?: string;
error?: string;
};
}
// ============================================================================
// Get Security Incidents Request
// ============================================================================
export interface IReq_GetSecurityIncidents extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetSecurityIncidents
> {
method: 'getSecurityIncidents';
request: {
identity?: authInterfaces.IIdentity;
type?: TSecurityEventType;
level?: TSecurityLogLevel;
limit?: number;
};
response: {
incidents: ISecurityIncident[];
total: number;
};
}
// ============================================================================
// Get Bounce Records Request
// ============================================================================
export interface IReq_GetBounceRecords extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetBounceRecords
> {
method: 'getBounceRecords';
request: {
identity?: authInterfaces.IIdentity;
limit?: number;
offset?: number;
};
response: {
records: IBounceRecord[];
suppressionList: string[];
total: number;
};
}
// ============================================================================
// Remove from Suppression List Request
// ============================================================================
export interface IReq_RemoveFromSuppressionList extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_RemoveFromSuppressionList
> {
method: 'removeFromSuppressionList';
request: {
identity?: authInterfaces.IIdentity;
email: string;
};
response: {
success: boolean;
error?: string;
};
}

View File

@@ -4,3 +4,4 @@ export * from './logs.js';
export * from './stats.js'; export * from './stats.js';
export * from './combined.stats.js'; export * from './combined.stats.js';
export * from './radius.js'; export * from './radius.js';
export * from './email-ops.js';

View File

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

View File

@@ -53,6 +53,20 @@ export interface INetworkState {
error: string | null; error: string | null;
} }
export interface IEmailOpsState {
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
queuedEmails: interfaces.requests.IEmailQueueItem[];
sentEmails: interfaces.requests.IEmailQueueItem[];
failedEmails: interfaces.requests.IEmailQueueItem[];
securityIncidents: interfaces.requests.ISecurityIncident[];
bounceRecords: interfaces.requests.IBounceRecord[];
suppressionList: string[];
selectedEmailId: string | null;
isLoading: boolean;
error: string | null;
lastUpdated: number;
}
// Create state parts with appropriate persistence // Create state parts with appropriate persistence
export const loginStatePart = await appState.getStatePart<ILoginState>( export const loginStatePart = await appState.getStatePart<ILoginState>(
'login', 'login',
@@ -60,7 +74,7 @@ export const loginStatePart = await appState.getStatePart<ILoginState>(
identity: null, identity: null,
isLoggedIn: false, isLoggedIn: false,
}, },
'soft' // Login state persists across sessions 'persistent' // Login state persists across browser sessions
); );
export const statsStatePart = await appState.getStatePart<IStatsState>( export const statsStatePart = await appState.getStatePart<IStatsState>(
@@ -121,6 +135,24 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
'soft' 'soft'
); );
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
'emailOps',
{
currentView: 'queued',
queuedEmails: [],
sentEmails: [],
failedEmails: [],
securityIncidents: [],
bounceRecords: [],
suppressionList: [],
selectedEmailId: null,
isLoading: false,
error: null,
lastUpdated: 0,
},
'soft'
);
// Actions for state management // Actions for state management
interface IActionContext { interface IActionContext {
identity: interfaces.data.IIdentity | null; identity: interfaces.data.IIdentity | null;
@@ -397,6 +429,238 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
} }
}); });
// ============================================================================
// Email Operations Actions
// ============================================================================
// Set Email Ops View Action
export const setEmailOpsViewAction = emailOpsStatePart.createAction<IEmailOpsState['currentView']>(
async (statePartArg, view) => {
return {
...statePartArg.getState(),
currentView: view,
};
}
);
// Fetch Queued Emails Action
export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetQueuedEmails
>('/typedrequest', 'getQueuedEmails');
const response = await request.fire({
identity: context.identity,
status: 'pending',
limit: 100,
});
return {
...currentState,
queuedEmails: response.items,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch queued emails',
};
}
});
// Fetch Sent Emails Action
export const fetchSentEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetSentEmails
>('/typedrequest', 'getSentEmails');
const response = await request.fire({
identity: context.identity,
limit: 100,
});
return {
...currentState,
sentEmails: response.items,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch sent emails',
};
}
});
// Fetch Failed Emails Action
export const fetchFailedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetFailedEmails
>('/typedrequest', 'getFailedEmails');
const response = await request.fire({
identity: context.identity,
limit: 100,
});
return {
...currentState,
failedEmails: response.items,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch failed emails',
};
}
});
// Fetch Security Incidents Action
export const fetchSecurityIncidentsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetSecurityIncidents
>('/typedrequest', 'getSecurityIncidents');
const response = await request.fire({
identity: context.identity,
limit: 100,
});
return {
...currentState,
securityIncidents: response.incidents,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch security incidents',
};
}
});
// Fetch Bounce Records Action
export const fetchBounceRecordsAction = emailOpsStatePart.createAction(async (statePartArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetBounceRecords
>('/typedrequest', 'getBounceRecords');
const response = await request.fire({
identity: context.identity,
limit: 100,
});
return {
...currentState,
bounceRecords: response.records,
suppressionList: response.suppressionList,
isLoading: false,
error: null,
lastUpdated: Date.now(),
};
} catch (error) {
return {
...currentState,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to fetch bounce records',
};
}
});
// Resend Failed Email Action
export const resendEmailAction = emailOpsStatePart.createAction<string>(async (statePartArg, emailId) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_ResendEmail
>('/typedrequest', 'resendEmail');
const response = await request.fire({
identity: context.identity,
emailId,
});
if (response.success) {
// Refresh failed emails list
await emailOpsStatePart.dispatchAction(fetchFailedEmailsAction, null);
await emailOpsStatePart.dispatchAction(fetchQueuedEmailsAction, null);
}
return statePartArg.getState();
} catch (error) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to resend email',
};
}
});
// Remove from Suppression List Action
export const removeFromSuppressionListAction = emailOpsStatePart.createAction<string>(
async (statePartArg, email) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_RemoveFromSuppressionList
>('/typedrequest', 'removeFromSuppressionList');
const response = await request.fire({
identity: context.identity,
email,
});
if (response.success) {
// Refresh bounce records
await emailOpsStatePart.dispatchAction(fetchBounceRecordsAction, null);
}
return statePartArg.getState();
} catch (error) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to remove from suppression list',
};
}
}
);
// Combined refresh action for efficient polling // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();

View File

@@ -1,5 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js'; import * as appstate from '../appstate.js';
import { appRouter } from '../router.js';
import { import {
DeesElement, DeesElement,
@@ -84,17 +85,55 @@ export class OpsDashboard extends DeesElement {
.select((stateArg) => stateArg) .select((stateArg) => stateArg)
.subscribe((uiState) => { .subscribe((uiState) => {
this.uiState = uiState; this.uiState = uiState;
// Sync appdash view when state changes (e.g., from URL navigation)
this.syncAppdashView(uiState.activeView);
}); });
this.rxSubscriptions.push(uiSubscription); this.rxSubscriptions.push(uiSubscription);
} }
/**
* Sync the dees-simple-appdash view selection with the current state.
* This is needed when the URL changes and we need to update the UI.
*/
private syncAppdashView(viewName: string): void {
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
if (!appDash) return;
const targetTab = this.viewTabs.find(t => t.name.toLowerCase() === viewName);
if (!targetTab) return;
// Check if we need to switch (avoid unnecessary updates)
if (appDash.selectedView === targetTab) return;
// Update the selected view programmatically
appDash.selectedView = targetTab;
// Update the displayed content
const content = appDash.shadowRoot?.querySelector('.appcontent');
if (content) {
if (appDash.currentView) {
appDash.currentView.remove();
}
const view = new targetTab.element();
content.appendChild(view);
appDash.currentView = view;
}
}
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host {
display: block;
width: 100%;
height: 100vh;
overflow: hidden;
}
.maincontainer { .maincontainer {
position: relative; position: relative;
width: 100vw; width: 100%;
height: 100vh; height: 100%;
} }
`, `,
]; ];
@@ -126,8 +165,9 @@ export class OpsDashboard extends DeesElement {
const appDash = this.shadowRoot.querySelector('dees-simple-appdash'); const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
if (appDash) { if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => { appDash.addEventListener('view-select', (e: CustomEvent) => {
const viewName = e.detail.view.name; const viewName = e.detail.view.name.toLowerCase();
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase()); // Use router for navigation instead of direct state update
appRouter.navigateToView(viewName);
}); });
// Handle logout event // Handle logout event
@@ -136,14 +176,20 @@ export class OpsDashboard extends DeesElement {
}); });
} }
// Handle initial state // Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState(); const loginState = appstate.loginStatePart.getState();
// Check initial login state if (loginState.identity?.jwt) {
if (loginState.identity) { // Verify JWT hasn't expired
if (loginState.identity.expiresAt > Date.now()) {
// JWT still valid, restore logged-in state
this.loginState = loginState; this.loginState = loginState;
await simpleLogin.switchToSlottedContent(); await simpleLogin.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null); await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
// JWT expired, clear the stored state
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} }
} }

View File

@@ -155,11 +155,10 @@ export class OpsViewConfig extends DeesElement {
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span> <span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
</div> </div>
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)} ${this.renderConfigSection('email', 'Email Configuration', this.configState.config?.email)}
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)} ${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config?.dns)}
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)} ${this.renderConfigSection('proxy', 'Proxy Configuration', this.configState.config?.proxy)}
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)} ${this.renderConfigSection('security', 'Security Configuration', this.configState.config?.security)}
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
` : html` ` : html`
<div class="errorMessage">No configuration loaded</div> <div class="errorMessage">No configuration loaded</div>
`} `}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,10 @@ import * as plugins from './plugins.js';
import { html } from '@design.estate/dees-element'; import { html } from '@design.estate/dees-element';
import './elements/index.js'; import './elements/index.js';
import { appRouter } from './router.js';
// Initialize router before rendering
appRouter.init();
plugins.deesElement.render(html` plugins.deesElement.render(html`
<ops-dashboard></ops-dashboard> <ops-dashboard></ops-dashboard>

181
ts_web/router.ts Normal file
View File

@@ -0,0 +1,181 @@
import * as plugins from './plugins.js';
import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'] as const;
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
export type TValidView = typeof validViews[number];
export type TValidEmailFolder = typeof validEmailFolders[number];
class AppRouter {
private router: InstanceType<typeof SmartRouter>;
private initialized = false;
private suppressStateUpdate = false;
constructor() {
this.router = new SmartRouter({ debug: false });
}
public init(): void {
if (this.initialized) return;
this.setupRoutes();
this.setupStateSync();
this.handleInitialRoute();
this.initialized = true;
}
private setupRoutes(): void {
// Main views
for (const view of validViews) {
if (view === 'emails') {
// Email root - default to queued
this.router.on('/emails', async () => {
this.updateViewState('emails');
this.updateEmailFolder('queued');
});
// Email with folder parameter
this.router.on('/emails/:folder', async (routeInfo) => {
const folder = routeInfo.params.folder as string;
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
this.updateViewState('emails');
this.updateEmailFolder(folder as TValidEmailFolder);
} else {
// Invalid folder, redirect to queued
this.navigateTo('/emails/queued');
}
});
} else {
this.router.on(`/${view}`, async () => {
this.updateViewState(view);
});
}
}
// Root redirect
this.router.on('/', async () => {
this.navigateTo('/overview');
});
}
private setupStateSync(): void {
// Sync URL when state changes programmatically (not from router)
appstate.uiStatePart.state.subscribe((uiState) => {
if (this.suppressStateUpdate) return;
const currentPath = window.location.pathname;
const expectedPath = this.getExpectedPath(uiState.activeView);
// Only update URL if it doesn't match current state
if (!currentPath.startsWith(expectedPath)) {
this.suppressStateUpdate = true;
if (uiState.activeView === 'emails') {
const emailState = appstate.emailOpsStatePart.getState();
this.router.pushUrl(`/emails/${emailState.currentView}`);
} else {
this.router.pushUrl(`/${uiState.activeView}`);
}
this.suppressStateUpdate = false;
}
});
}
private getExpectedPath(view: string): string {
if (view === 'emails') {
return '/emails';
}
return `/${view}`;
}
private handleInitialRoute(): void {
const path = window.location.pathname;
if (!path || path === '/') {
// Redirect root to overview
this.router.pushUrl('/overview');
} else {
// Parse current path and update state
const segments = path.split('/').filter(Boolean);
const view = segments[0];
if (validViews.includes(view as TValidView)) {
this.updateViewState(view as TValidView);
if (view === 'emails' && segments[1]) {
const folder = segments[1];
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
this.updateEmailFolder(folder as TValidEmailFolder);
} else {
this.updateEmailFolder('queued');
}
} else if (view === 'emails') {
this.updateEmailFolder('queued');
}
} else {
// Invalid view, redirect to overview
this.router.pushUrl('/overview');
}
}
}
private updateViewState(view: string): void {
this.suppressStateUpdate = true;
const currentState = appstate.uiStatePart.getState();
if (currentState.activeView !== view) {
appstate.uiStatePart.setState({
...currentState,
activeView: view,
});
}
this.suppressStateUpdate = false;
}
private updateEmailFolder(folder: TValidEmailFolder): void {
this.suppressStateUpdate = true;
const currentState = appstate.emailOpsStatePart.getState();
if (currentState.currentView !== folder) {
appstate.emailOpsStatePart.setState({
...currentState,
currentView: folder as appstate.IEmailOpsState['currentView'],
});
}
this.suppressStateUpdate = false;
}
public navigateTo(path: string): void {
this.router.pushUrl(path);
}
public navigateToView(view: string): void {
if (validViews.includes(view as TValidView)) {
this.navigateTo(`/${view}`);
} else {
this.navigateTo('/overview');
}
}
public navigateToEmailFolder(folder: string): void {
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
this.navigateTo(`/emails/${folder}`);
} else {
this.navigateTo('/emails/queued');
}
}
public getCurrentView(): string {
return appstate.uiStatePart.getState().activeView;
}
public getCurrentEmailFolder(): string {
return appstate.emailOpsStatePart.getState().currentView;
}
public destroy(): void {
this.router.destroy();
this.initialized = false;
}
}
export const appRouter = new AppRouter();