BREAKING CHANGE(deps): upgrade major dependencies, migrate action.target to action.targets (array), adapt to SmartRequest API changes, and add RADIUS server support
This commit is contained in:
BIN
.playwright-mcp/dcrouter-scrollbar-issue.png
Normal file
BIN
.playwright-mcp/dcrouter-scrollbar-issue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-10-23-737Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-10-23-737Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-11-19-449Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-11-19-449Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-12-03-126Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-12-03-126Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-12-15-576Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-12-15-576Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -1,5 +1,10 @@
|
||||
# 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)
|
||||
add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers
|
||||
|
||||
|
||||
18
package.json
18
package.json
@@ -22,16 +22,16 @@
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.0.1",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/node": "^25.2.0",
|
||||
"node-forge": "^1.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.0.19",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||
"@design.estate/dees-catalog": "^1.10.10",
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@api.global/typedsocket": "^4.1.0",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.41.5",
|
||||
"@design.estate/dees-element": "^2.0.45",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
@@ -45,11 +45,11 @@
|
||||
"@push.rocks/smartmail": "^2.2.0",
|
||||
"@push.rocks/smartmetrics": "^2.0.10",
|
||||
"@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/smartproxy": "^19.6.15",
|
||||
"@push.rocks/smartproxy": "^22.4.2",
|
||||
"@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/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.0.27",
|
||||
@@ -61,7 +61,7 @@
|
||||
"lru-cache": "^11.2.5",
|
||||
"mailauth": "^4.12.0",
|
||||
"mailparser": "^3.9.3",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"mail service",
|
||||
|
||||
219
pnpm-lock.yaml
generated
219
pnpm-lock.yaml
generated
@@ -15,17 +15,17 @@ importers:
|
||||
specifier: ^3.0.19
|
||||
version: 3.0.19
|
||||
'@api.global/typedserver':
|
||||
specifier: ^3.0.74
|
||||
version: 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(@tiptap/pm@2.27.2)
|
||||
'@api.global/typedsocket':
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.1(@push.rocks/smartserve@2.0.1)
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@apiclient.xyz/cloudflare':
|
||||
specifier: ^6.4.1
|
||||
version: 6.4.3
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^1.10.10
|
||||
version: 1.12.4(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.41.5
|
||||
version: 3.41.5(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.0.45
|
||||
version: 2.1.6
|
||||
@@ -37,7 +37,7 @@ importers:
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartacme':
|
||||
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':
|
||||
specifier: ^5.15.1
|
||||
version: 5.16.7(socks@2.8.7)
|
||||
@@ -66,20 +66,20 @@ importers:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
'@push.rocks/smartpath':
|
||||
specifier: ^5.0.5
|
||||
version: 5.1.0
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@push.rocks/smartpromise':
|
||||
specifier: ^4.0.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^19.6.15
|
||||
version: 19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
specifier: ^22.4.2
|
||||
version: 22.4.2(socks@2.8.7)
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.0.3
|
||||
version: 1.1.0
|
||||
'@push.rocks/smartrequest':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
'@push.rocks/smartrule':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
@@ -114,8 +114,8 @@ importers:
|
||||
specifier: ^3.9.3
|
||||
version: 3.9.3
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^4.1.2
|
||||
@@ -133,8 +133,8 @@ importers:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@tiptap/pm@2.27.2)
|
||||
'@types/node':
|
||||
specifier: ^25.1.0
|
||||
version: 25.1.0
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0
|
||||
node-forge:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
@@ -172,6 +172,9 @@ packages:
|
||||
'@apiclient.xyz/cloudflare@6.4.3':
|
||||
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':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -359,11 +362,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@1.12.4':
|
||||
resolution: {integrity: sha512-tzNW3b1BQkbE7W2DcwFqXHy+igHzxMHoR+awCAE4/EzHl8kOJc4jF1RNyCe34LsuNaELgZ95Hde/HGgEC8JasA==}
|
||||
|
||||
'@design.estate/dees-catalog@3.41.4':
|
||||
resolution: {integrity: sha512-tVut61OuMF+o/1dzcKEvfom6sl8dMC5WzxIevN9jVq4sQgMO9DOrFd+UfKwRL9FfLZeqAFyxJDzU8389e4Pg0Q==}
|
||||
'@design.estate/dees-catalog@3.41.5':
|
||||
resolution: {integrity: sha512-2LOUh92h2ndzlEKOyDqGE2Mdjhmxt6ZeAqHt5KKslNHzmNhdWFKUe6C1Vm2nU6vRvFLXXC/ex56KSP3XcCPD8g==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -374,9 +374,6 @@ packages:
|
||||
'@design.estate/dees-element@2.1.6':
|
||||
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':
|
||||
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
||||
|
||||
@@ -1066,8 +1063,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@19.6.17':
|
||||
resolution: {integrity: sha512-5y6lVxlHXoVQXAQLr5S+2ifxZf9EID32twyeuZTS9tDyof0wJKppLzKQepwB7hfQXS2J06JBN7oa9n0mguELBg==}
|
||||
'@push.rocks/smartproxy@22.4.2':
|
||||
resolution: {integrity: sha512-JdDa1VxGOnWfF5HuJRvkX3/zHuIKz+IV9n/XOsNZQA9zMZdLVlWPqjGio9GLWsPOWA2l1YZKymjMH4ybPbGQtA==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -1876,6 +1873,10 @@ packages:
|
||||
'@types/minimatch@5.1.2':
|
||||
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':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
@@ -1894,8 +1895,8 @@ packages:
|
||||
'@types/node@22.19.7':
|
||||
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
|
||||
|
||||
'@types/node@25.1.0':
|
||||
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||
'@types/node@25.2.0':
|
||||
resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
|
||||
|
||||
'@types/pidusage@2.0.5':
|
||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||
@@ -1969,9 +1970,6 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@webcontainer/api@1.2.0':
|
||||
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3':
|
||||
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||
|
||||
@@ -3097,9 +3095,6 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lucide@0.544.0:
|
||||
resolution: {integrity: sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==}
|
||||
|
||||
lucide@0.563.0:
|
||||
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
|
||||
|
||||
@@ -3343,9 +3338,6 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||
|
||||
@@ -4206,8 +4198,8 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
@@ -4438,7 +4430,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@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
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4485,7 +4477,7 @@ snapshots:
|
||||
'@push.rocks/isohash': 2.0.1
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
'@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/smarturl': 3.1.0
|
||||
optionalDependencies:
|
||||
@@ -4523,6 +4515,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@@ -5028,43 +5032,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@1.12.4(@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)':
|
||||
'@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
|
||||
@@ -5144,18 +5112,6 @@ snapshots:
|
||||
- supports-color
|
||||
- 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':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
@@ -5920,7 +5876,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@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:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@apiclient.xyz/cloudflare': 6.4.3
|
||||
@@ -5942,7 +5898,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
@@ -6454,22 +6409,22 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@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/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartnetwork': 4.4.0
|
||||
'@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/smartstring': 4.1.0
|
||||
'@push.rocks/taskbuffer': 3.5.0
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/minimatch': 6.0.0
|
||||
'@types/ws': 8.18.1
|
||||
minimatch: 10.1.1
|
||||
pretty-ms: 9.3.0
|
||||
@@ -6478,7 +6433,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -6592,7 +6546,7 @@ snapshots:
|
||||
'@push.rocks/webrequest': 4.0.1
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
|
||||
'@push.rocks/smartsocket@2.1.0(@push.rocks/smartserve@2.0.1)':
|
||||
'@push.rocks/smartsocket@2.1.0':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
@@ -6611,7 +6565,6 @@ snapshots:
|
||||
socket.io-client: 4.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bufferutil
|
||||
- react
|
||||
- supports-color
|
||||
@@ -7462,27 +7415,27 @@ snapshots:
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@@ -7490,7 +7443,7 @@ snapshots:
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
@@ -7498,7 +7451,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
@@ -7511,17 +7464,17 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.6':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7543,18 +7496,18 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/mailparser@3.4.6':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
@@ -7572,20 +7525,24 @@ snapshots:
|
||||
|
||||
'@types/minimatch@5.1.2': {}
|
||||
|
||||
'@types/minimatch@6.0.0':
|
||||
dependencies:
|
||||
minimatch: 10.1.1
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/node@18.19.130':
|
||||
dependencies:
|
||||
@@ -7595,7 +7552,7 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.1.0':
|
||||
'@types/node@25.2.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
@@ -7615,22 +7572,22 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -7656,17 +7613,15 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@webcontainer/api@1.2.0': {}
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3': {}
|
||||
|
||||
'@zone-eu/mailsplit@5.4.8':
|
||||
@@ -8146,7 +8101,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.19
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
@@ -8918,8 +8873,6 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lucide@0.544.0: {}
|
||||
|
||||
lucide@0.563.0: {}
|
||||
|
||||
mailauth@4.12.0:
|
||||
@@ -9352,8 +9305,6 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.2.7
|
||||
@@ -10372,7 +10323,7 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
uuid@13.0.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
|
||||
134
readme.hints.md
134
readme.hints.md
@@ -1,5 +1,71 @@
|
||||
# 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)
|
||||
|
||||
### Overview
|
||||
@@ -1131,3 +1197,71 @@ The throughput was showing 0 because:
|
||||
4. Updated frontend to call the new endpoint for complete network metrics
|
||||
|
||||
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
|
||||
37
readme.md
37
readme.md
@@ -40,6 +40,11 @@ A comprehensive traffic routing solution that provides unified gateway capabilit
|
||||
- **DKIM, SPF, DMARC** authentication and verification
|
||||
- **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**
|
||||
- **Connection pooling** and efficient resource management
|
||||
- **Load balancing** with automatic failover
|
||||
@@ -79,7 +84,7 @@ const router = new DcRouter({
|
||||
match: { domains: ['example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.10', port: 8080 },
|
||||
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
}
|
||||
@@ -271,10 +276,10 @@ interface IRouteConfig {
|
||||
};
|
||||
action: {
|
||||
type: 'forward' | 'redirect' | 'serve';
|
||||
target?: {
|
||||
targets?: Array<{
|
||||
host: string;
|
||||
port: number | 'preserve' | ((context: any) => number);
|
||||
};
|
||||
}>;
|
||||
tls?: {
|
||||
mode: 'terminate' | 'passthrough';
|
||||
certificate?: 'auto' | string;
|
||||
@@ -640,15 +645,7 @@ const routes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
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;
|
||||
}
|
||||
},
|
||||
targets: [{ host: '192.168.1.20', port: 8080 }],
|
||||
tls: {
|
||||
mode: 'terminate',
|
||||
certificate: 'auto'
|
||||
@@ -687,10 +684,7 @@ const tcpRoutes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '192.168.1.30',
|
||||
port: 'preserve'
|
||||
},
|
||||
targets: [{ host: '192.168.1.30', port: 'preserve' }],
|
||||
security: {
|
||||
ipAllowList: ['192.168.1.0/24']
|
||||
}
|
||||
@@ -706,10 +700,7 @@ const tcpRoutes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '192.168.1.40',
|
||||
port: 8443
|
||||
},
|
||||
targets: [{ host: '192.168.1.40', port: 8443 }],
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
@@ -893,7 +884,7 @@ const router = new DcRouter({
|
||||
match: { domains: ['example.com', 'www.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.10', port: 80 },
|
||||
targets: [{ host: '192.168.1.10', port: 80 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
},
|
||||
@@ -905,7 +896,7 @@ const router = new DcRouter({
|
||||
match: { domains: ['api.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.20', port: 8080 },
|
||||
targets: [{ host: '192.168.1.20', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
},
|
||||
@@ -916,7 +907,7 @@ const router = new DcRouter({
|
||||
match: { ports: [{ from: 8000, to: 8999 }] },
|
||||
action: {
|
||||
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'] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '2.13.0',
|
||||
version: '3.0.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -517,10 +517,10 @@ export class DcRouter {
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: route.action.type === 'forward' && route.action.forward ? {
|
||||
targets: route.action.type === 'forward' && route.action.forward ? [{
|
||||
host: route.action.forward.host,
|
||||
port: route.action.forward.port || 25
|
||||
} : undefined,
|
||||
}] : undefined,
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class OpsServer {
|
||||
private securityHandler: handlers.SecurityHandler;
|
||||
private statsHandler: handlers.StatsHandler;
|
||||
private radiusHandler: handlers.RadiusHandler;
|
||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
@@ -55,6 +56,7 @@ export class OpsServer {
|
||||
this.securityHandler = new handlers.SecurityHandler(this);
|
||||
this.statsHandler = new handlers.StatsHandler(this);
|
||||
this.radiusHandler = new handlers.RadiusHandler(this);
|
||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||
|
||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class AdminHandler {
|
||||
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({
|
||||
userId: user.id,
|
||||
|
||||
325
ts/opsserver/handlers/email-ops.handler.ts
Normal file
325
ts/opsserver/handlers/email-ops.handler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './logs.handler.js';
|
||||
export * from './security.handler.js';
|
||||
export * from './stats.handler.js';
|
||||
export * from './radius.handler.js';
|
||||
export * from './email-ops.handler.js';
|
||||
@@ -68,13 +68,13 @@ export class SmsService {
|
||||
recipients: [{ msisdn: toNumber }],
|
||||
};
|
||||
|
||||
const resp = await plugins.smartrequest.SmartRequestClient.create()
|
||||
const resp = await plugins.smartrequest.SmartRequest.create()
|
||||
.url('https://gatewayapi.com/rest/mtsms')
|
||||
.header('Authorization', `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`)
|
||||
.header('Content-Type', 'application/json')
|
||||
.json(payload)
|
||||
.post();
|
||||
const json = resp.body;
|
||||
const json = await resp.json();
|
||||
logger.log('info', `sent an sms to ${toNumber} with text '${messageText}'`, {
|
||||
eventType: 'sentSms',
|
||||
sms: {
|
||||
|
||||
239
ts_interfaces/requests/email-ops.ts
Normal file
239
ts_interfaces/requests/email-ops.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './logs.js';
|
||||
export * from './stats.js';
|
||||
export * from './combined.stats.js';
|
||||
export * from './radius.js';
|
||||
export * from './email-ops.js';
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '2.13.0',
|
||||
version: '3.0.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -53,6 +53,20 @@ export interface INetworkState {
|
||||
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
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
@@ -60,7 +74,7 @@ export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'soft' // Login state persists across sessions
|
||||
'persistent' // Login state persists across browser sessions
|
||||
);
|
||||
|
||||
export const statsStatePart = await appState.getStatePart<IStatsState>(
|
||||
@@ -121,6 +135,24 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
'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
|
||||
interface IActionContext {
|
||||
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
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -84,17 +85,55 @@ export class OpsDashboard extends DeesElement {
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
// Sync appdash view when state changes (e.g., from URL navigation)
|
||||
this.syncAppdashView(uiState.activeView);
|
||||
});
|
||||
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 = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -126,8 +165,9 @@ export class OpsDashboard extends DeesElement {
|
||||
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
|
||||
if (appDash) {
|
||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||
const viewName = e.detail.view.name;
|
||||
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
|
||||
const viewName = e.detail.view.name.toLowerCase();
|
||||
// Use router for navigation instead of direct state update
|
||||
appRouter.navigateToView(viewName);
|
||||
});
|
||||
|
||||
// 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();
|
||||
// Check initial login state
|
||||
if (loginState.identity) {
|
||||
if (loginState.identity?.jwt) {
|
||||
// Verify JWT hasn't expired
|
||||
if (loginState.identity.expiresAt > Date.now()) {
|
||||
// JWT still valid, restore logged-in state
|
||||
this.loginState = loginState;
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
} else {
|
||||
// JWT expired, clear the stored state
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
|
||||
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)}
|
||||
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)}
|
||||
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)}
|
||||
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)}
|
||||
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
|
||||
${this.renderConfigSection('email', 'Email Configuration', this.configState.config?.email)}
|
||||
${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)}
|
||||
` : html`
|
||||
<div class="errorMessage">No configuration loaded</div>
|
||||
`}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@ import * as plugins from './plugins.js';
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
import './elements/index.js';
|
||||
import { appRouter } from './router.js';
|
||||
|
||||
// Initialize router before rendering
|
||||
appRouter.init();
|
||||
|
||||
plugins.deesElement.render(html`
|
||||
<ops-dashboard></ops-dashboard>
|
||||
|
||||
181
ts_web/router.ts
Normal file
181
ts_web/router.ts
Normal 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();
|
||||
Reference in New Issue
Block a user