Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc0371f7e | |||
| b212662764 | |||
| 776c65a18c | |||
| 5f6ec63770 | |||
| 1b4cc0567f | |||
| 22de50b544 |
@@ -14,6 +14,37 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 2026-05-31 - 13.41.0
|
||||
|
||||
### Features
|
||||
|
||||
- add RemoteIngress hub settings management (remoteingress)
|
||||
- Persist hub-level RemoteIngress performance settings with validation and seed defaults from config
|
||||
- Add typed read/update handlers and web UI controls for hub performance settings
|
||||
- Restart the tunnel hub after hub setting updates so new performance defaults take effect
|
||||
- Serialize RemoteIngress lifecycle tasks, edge mutations, route syncs, and stop/start operations to avoid hub race conditions
|
||||
|
||||
## 2026-05-31 - 13.40.3
|
||||
|
||||
### Fixes
|
||||
|
||||
- bump smartproxy and remoteingress dependencies (deps)
|
||||
- Bumped @push.rocks/smartproxy from ^27.12.1 to ^27.12.2
|
||||
- Bumped @serve.zone/remoteingress from ^4.22.2 to ^4.22.3
|
||||
- Updated dependency versions in both package.json and deno.json
|
||||
|
||||
## 2026-05-31 - 13.40.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- ensure source profiles fully own route security (routes)
|
||||
- Resolve profile-backed routes by cloning source profile security instead of merging inline route overrides
|
||||
- Clear stale route security when a source profile reference is removed without explicit replacement security
|
||||
- Add a migration to rematerialize persisted profile-backed route security
|
||||
|
||||
## 2026-05-31 - 13.40.1
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"version": "13.40.1",
|
||||
"version": "13.41.0",
|
||||
"exports": "./binary/dcrouter.ts",
|
||||
"compile": {
|
||||
"include": [
|
||||
@@ -31,7 +31,7 @@
|
||||
"@push.rocks/smartnetwork": "npm:@push.rocks/smartnetwork@^4.7.2",
|
||||
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0",
|
||||
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.4",
|
||||
"@push.rocks/smartproxy": "npm:@push.rocks/smartproxy@^27.12.1",
|
||||
"@push.rocks/smartproxy": "npm:@push.rocks/smartproxy@^27.12.2",
|
||||
"@push.rocks/smartradius": "npm:@push.rocks/smartradius@^1.1.2",
|
||||
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^5.0.3",
|
||||
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
|
||||
@@ -40,7 +40,7 @@
|
||||
"@push.rocks/smartvpn": "npm:@push.rocks/smartvpn@1.20.0",
|
||||
"@push.rocks/taskbuffer": "npm:@push.rocks/taskbuffer@^8.0.2",
|
||||
"@serve.zone/interfaces": "npm:@serve.zone/interfaces@^5.8.0",
|
||||
"@serve.zone/remoteingress": "npm:@serve.zone/remoteingress@^4.22.2",
|
||||
"@serve.zone/remoteingress": "npm:@serve.zone/remoteingress@^4.22.3",
|
||||
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.5.1",
|
||||
"lru-cache": "npm:lru-cache@^11.4.0",
|
||||
"qrcode": "npm:qrcode@^1.5.4",
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "13.40.1",
|
||||
"version": "13.41.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -61,7 +61,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.7.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartproxy": "^27.12.1",
|
||||
"@push.rocks/smartproxy": "^27.12.2",
|
||||
"@push.rocks/smartradius": "^1.3.0",
|
||||
"@push.rocks/smartrequest": "^5.0.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.12.4",
|
||||
"@serve.zone/interfaces": "^5.8.0",
|
||||
"@serve.zone/remoteingress": "^4.22.2",
|
||||
"@serve.zone/remoteingress": "^4.22.3",
|
||||
"@tsclass/tsclass": "^9.5.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"lru-cache": "^11.4.0",
|
||||
|
||||
Generated
+59
-66
@@ -84,8 +84,8 @@ importers:
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^27.12.1
|
||||
version: 27.12.1
|
||||
specifier: ^27.12.2
|
||||
version: 27.12.2
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
@@ -114,8 +114,8 @@ importers:
|
||||
specifier: ^5.8.0
|
||||
version: 5.8.0
|
||||
'@serve.zone/remoteingress':
|
||||
specifier: ^4.22.2
|
||||
version: 4.22.2
|
||||
specifier: ^4.22.3
|
||||
version: 4.22.3
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1
|
||||
@@ -181,7 +181,6 @@ packages:
|
||||
|
||||
'@apiglobal/typedrequest-interfaces@1.0.20':
|
||||
resolution: {integrity: sha512-ybsDtavYbzGJYSLodSbkxDvSLYtfMzBTuNZDJpiANt1rZA2MO/GCq8zk5MVLlrUUQIr/7oxPGWqxi1QDwR+RHQ==}
|
||||
deprecated: This package has been replaced by @api.global/typedrequest-interfaces
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
@@ -1235,11 +1234,9 @@ packages:
|
||||
|
||||
'@push.rocks/isohash@2.0.1':
|
||||
resolution: {integrity: sha512-UulhEui8O9Ei9fSqTldsB73TUmAFNqEBk82tHsJSLLpNK9gJZQE82iaSNsQUakoUQ2c9KueueMfwC3IoDaYRrQ==}
|
||||
deprecated: This package has moved to @push.rocks/smarthash
|
||||
|
||||
'@push.rocks/isounique@1.0.5':
|
||||
resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==}
|
||||
deprecated: This package has been replaced by @push.rocks/smartunique
|
||||
|
||||
'@push.rocks/levelcache@3.2.2':
|
||||
resolution: {integrity: sha512-g44xp3XmtSPlcTHQ8qoaNV0AK7w4cuLd6h7sGXXxldN3NLgjOUpUqnnyDBU9i5hpIIxqssxe8WRQz10bi9W+tA==}
|
||||
@@ -1432,8 +1429,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.4':
|
||||
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
|
||||
|
||||
'@push.rocks/smartproxy@27.12.1':
|
||||
resolution: {integrity: sha512-B1QNyGzwFea8fE2vvXO0iDzYrTfe3HcEnhPhNi6hVnmdSPe1yhNYUu5tm1CKLeCoXu/EVkAUkEFv/+d7gKa9EA==}
|
||||
'@push.rocks/smartproxy@27.12.2':
|
||||
resolution: {integrity: sha512-q97n/UAhfvyds6MhTUAhV5OC7x3Eaot+IN25hW6StyvrxR/odg3/g2UDAJmHoD5X0tKwIhouFd/b8Nwx0p94cg==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.6':
|
||||
resolution: {integrity: sha512-G+8cyDERvbXQcb9Sd8lnYdWYz8b3Mv2LfFf1ULmucDqQhcRHvxrWX/dKsvBZrwKPR4Wg+795Dyd+E1iOOh3tHw==}
|
||||
@@ -1522,11 +1519,10 @@ packages:
|
||||
|
||||
'@push.rocks/webstream@1.0.10':
|
||||
resolution: {integrity: sha512-45CcR0I4/9v0qSjLvz2dYTGMkR0YP3x66ItpStdad5hidJm86t1lfHF06d0oiEvJTpvQkeyIX/8YKAumf21d/Q==}
|
||||
deprecated: This package has been deprecated and replaced by @push.rocks/smartstream/web
|
||||
|
||||
'@pushrocks/isounique@1.0.5':
|
||||
resolution: {integrity: sha512-XYeoKGkmIdsWX64NlPA1fuA41n/1bQ7LdYXytlU/QqYeW7ojgA0ARRhBSh/2phL6o0Jpw6K/7gJ8jc7ab/Tc+w==}
|
||||
deprecated: This package has been replaced by @push.rocks/smartunique
|
||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/isounique
|
||||
|
||||
'@pushrocks/smartenv@5.0.5':
|
||||
resolution: {integrity: sha512-VWON1OJ4qV2/9hzJbgRquRekaO9am3b8W82tgCwgO6LBg23ea2tanfd+gESVMbRFduxHVoFLvlhSBcDGM5zsLA==}
|
||||
@@ -1723,8 +1719,8 @@ packages:
|
||||
'@serve.zone/interfaces@5.8.0':
|
||||
resolution: {integrity: sha512-0ekSKUL/b44wmmzuCRANzrjaJRAHtkqiL8cPiMASEs7UJBDqbJCrgtrlJK84pz5dxBz3jTcdznNd5qjB8c6H0A==}
|
||||
|
||||
'@serve.zone/remoteingress@4.22.2':
|
||||
resolution: {integrity: sha512-fE7dQkhHZqNHztu5dTy91CuM+e9NBlYvB0iJRV/4MiSP/5z+SBiwyAmR1i6RTHoLogj/XCPcwy8N+mSbb2oI9A==}
|
||||
'@serve.zone/remoteingress@4.22.3':
|
||||
resolution: {integrity: sha512-VUI2VTMHVjju92FXjPe0EQ7op2EyqCr+JQIIGkjxnvqE9aAV9ZtaNzI7y4WwltYNo9rfaa/Bdd8+2EKUYYCD6g==}
|
||||
hasBin: true
|
||||
|
||||
'@smithy/chunked-blob-reader-native@4.2.3':
|
||||
@@ -2250,7 +2246,7 @@ packages:
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
ansi-256-colors@1.1.0:
|
||||
resolution: {integrity: sha512-roJI/AVBdJIhcohHDNXUoFYsCZG4MZIs5HtKNgVKY5QzqQoQJe+o0ouiqZDaSC+ggKdBVcuSwlSdJckrrlm3/A==}
|
||||
resolution: {integrity: sha1-kQ3lDvzHwJ49gvL4er1rcAwYgYo=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
@@ -2287,7 +2283,7 @@ packages:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
|
||||
|
||||
await-to-js@3.0.0:
|
||||
resolution: {integrity: sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==}
|
||||
@@ -2383,10 +2379,10 @@ packages:
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
|
||||
|
||||
buffer-json@2.0.0:
|
||||
resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==}
|
||||
@@ -2411,7 +2407,7 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
|
||||
camel-case@3.0.0:
|
||||
resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==}
|
||||
resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
@@ -2477,7 +2473,7 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clone@1.0.4:
|
||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
cloudflare@5.2.0:
|
||||
@@ -2491,7 +2487,7 @@ packages:
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
@@ -2507,7 +2503,7 @@ packages:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=}
|
||||
|
||||
config-chain@1.1.13:
|
||||
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
||||
@@ -2556,7 +2552,7 @@ packages:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
@@ -2590,7 +2586,7 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
dequal@2.0.3:
|
||||
@@ -2692,7 +2688,7 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-string-regexp@1.0.5:
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
escape-string-regexp@4.0.0:
|
||||
@@ -2785,7 +2781,7 @@ packages:
|
||||
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=}
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
@@ -2835,7 +2831,7 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
format@0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
resolution: {integrity: sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
formdata-node@4.4.1:
|
||||
@@ -2901,7 +2897,7 @@ packages:
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
@@ -2964,7 +2960,7 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=}
|
||||
|
||||
ibantools@4.5.4:
|
||||
resolution: {integrity: sha512-6jX1gh4aH6XH+o0ey+wtkMTzkcvsEta7DakIOZSng9voZYpMw3U+gK1+tZChk3aRcPcloEt0NOzksjaRZiqXbw==}
|
||||
@@ -3009,7 +3005,7 @@ packages:
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=}
|
||||
|
||||
is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
@@ -3057,7 +3053,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
|
||||
|
||||
isexe@4.0.0:
|
||||
resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
|
||||
@@ -3142,28 +3138,28 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lodash.clonedeep@4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=}
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
resolution: {integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=}
|
||||
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
resolution: {integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=}
|
||||
|
||||
lodash.isinteger@4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
resolution: {integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=}
|
||||
|
||||
lodash.isnumber@3.0.3:
|
||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||
resolution: {integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=}
|
||||
|
||||
lodash.isstring@4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=}
|
||||
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
|
||||
|
||||
log-symbols@3.0.0:
|
||||
resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==}
|
||||
@@ -3173,7 +3169,7 @@ packages:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
lower-case@1.1.4:
|
||||
resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
|
||||
resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
@@ -3483,7 +3479,6 @@ packages:
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
@@ -3525,7 +3520,7 @@ packages:
|
||||
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
||||
|
||||
onetime@5.1.2:
|
||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||
@@ -3543,11 +3538,11 @@ packages:
|
||||
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
||||
|
||||
os-tmpdir@1.0.2:
|
||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
p-finally@1.0.0:
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
@@ -3589,7 +3584,7 @@ packages:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
param-case@2.1.1:
|
||||
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
|
||||
resolution: {integrity: sha1-35T9jPZTHs915r75oIWPvHK+Ikc=}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
@@ -3641,6 +3636,7 @@ packages:
|
||||
resolution: {integrity: sha512-QErPemxRHDI2RUli3+9/mv4V6Ib9VWI+UoP2S82yXEQtoXzWvu9NSjjo3vyiUiVJv+CJFuzNiKUI+UFFUdv8Lg==}
|
||||
engines: {node: '>=20.18.0'}
|
||||
hasBin: true
|
||||
bundledDependencies: []
|
||||
|
||||
pdfjs-dist@4.10.38:
|
||||
resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==}
|
||||
@@ -3654,7 +3650,7 @@ packages:
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
@@ -3753,7 +3749,7 @@ packages:
|
||||
resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==}
|
||||
|
||||
proto-list@1.2.4:
|
||||
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
|
||||
|
||||
proxy-agent@6.5.0:
|
||||
resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==}
|
||||
@@ -3774,7 +3770,7 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@1.4.1:
|
||||
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
||||
resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
@@ -3833,7 +3829,7 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
|
||||
relateurl@0.2.7:
|
||||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||
resolution: {integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
remark-frontmatter@5.0.0:
|
||||
@@ -3856,7 +3852,7 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
@@ -3916,7 +3912,7 @@ packages:
|
||||
hasBin: true
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
@@ -3981,7 +3977,7 @@ packages:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
sparse-bitfield@3.0.3:
|
||||
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
|
||||
resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=}
|
||||
|
||||
spawn-wrap@3.0.0:
|
||||
resolution: {integrity: sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==}
|
||||
@@ -4009,7 +4005,7 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
resolution: {integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
strnum@1.1.2:
|
||||
@@ -4085,7 +4081,7 @@ packages:
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
@@ -4193,7 +4189,7 @@ packages:
|
||||
resolution: {integrity: sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==}
|
||||
|
||||
upper-case@1.1.3:
|
||||
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
|
||||
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
|
||||
|
||||
url@0.11.4:
|
||||
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
|
||||
@@ -4203,7 +4199,7 @@ packages:
|
||||
resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
|
||||
|
||||
uuid@13.0.2:
|
||||
resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==}
|
||||
@@ -4215,7 +4211,6 @@ packages:
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
vfile-message@4.0.3:
|
||||
@@ -4228,7 +4223,7 @@ packages:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
@@ -4238,7 +4233,7 @@ packages:
|
||||
resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
@@ -4253,7 +4248,7 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
@@ -4269,7 +4264,7 @@ packages:
|
||||
hasBin: true
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
@@ -4280,7 +4275,7 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
|
||||
|
||||
ws@8.20.0:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
@@ -4307,13 +4302,11 @@ packages:
|
||||
|
||||
xterm-addon-fit@0.8.0:
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
|
||||
xterm@5.3.0:
|
||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
@@ -4348,7 +4341,7 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yauzl@2.10.0:
|
||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||
resolution: {integrity: sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=}
|
||||
|
||||
yauzl@3.3.0:
|
||||
resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==}
|
||||
@@ -6703,7 +6696,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.4': {}
|
||||
|
||||
'@push.rocks/smartproxy@27.12.1':
|
||||
'@push.rocks/smartproxy@27.12.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
@@ -7092,7 +7085,7 @@ snapshots:
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@tsclass/tsclass': 9.5.1
|
||||
|
||||
'@serve.zone/remoteingress@4.22.2':
|
||||
'@serve.zone/remoteingress@4.22.3':
|
||||
dependencies:
|
||||
'@push.rocks/qenv': 6.1.4
|
||||
'@push.rocks/smartnftables': 1.2.0
|
||||
|
||||
+124
-11
@@ -12,13 +12,77 @@ function setPath(target: Record<string, any>, path: string, value: unknown): voi
|
||||
cursor[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function getPath(target: Record<string, any>, path: string): unknown {
|
||||
let cursor: any = target;
|
||||
for (const part of path.split('.')) {
|
||||
if (cursor === null || cursor === undefined) return undefined;
|
||||
cursor = cursor[part];
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function applySet(document: Record<string, any>, set: Record<string, unknown>): void {
|
||||
for (const [key, value] of Object.entries(set)) {
|
||||
setPath(document, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function createFakeDb(currentVersion: string) {
|
||||
function matchesQuery(document: Record<string, any>, query: Record<string, any>): boolean {
|
||||
for (const [key, expected] of Object.entries(query)) {
|
||||
const actual = getPath(document, key);
|
||||
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
||||
if ('$exists' in expected) {
|
||||
const exists = actual !== undefined;
|
||||
if (exists !== Boolean(expected.$exists)) return false;
|
||||
continue;
|
||||
}
|
||||
if ('$type' in expected) {
|
||||
if (expected.$type === 'string' && typeof actual !== 'string') return false;
|
||||
continue;
|
||||
}
|
||||
if ('$in' in expected) {
|
||||
if (!Array.isArray(expected.$in) || !expected.$in.includes(actual)) return false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (actual !== expected) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createFakeCollection(documents: Array<Record<string, any>> = []) {
|
||||
return {
|
||||
find: (query: Record<string, any> = {}) => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for (const document of documents) {
|
||||
if (matchesQuery(document, query)) {
|
||||
yield structuredClone(document);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
updateMany: async (query: Record<string, any>, update: any) => {
|
||||
let modifiedCount = 0;
|
||||
for (const document of documents) {
|
||||
if (!matchesQuery(document, query)) continue;
|
||||
applySet(document, update.$set || {});
|
||||
modifiedCount++;
|
||||
}
|
||||
return { modifiedCount };
|
||||
},
|
||||
updateOne: async (query: Record<string, any>, update: any) => {
|
||||
const document = documents.find((candidate) => matchesQuery(candidate, query));
|
||||
if (!document) return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };
|
||||
applySet(document, update.$set || {});
|
||||
return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeDb(
|
||||
currentVersion: string,
|
||||
collections: Record<string, Array<Record<string, any>>> = {},
|
||||
) {
|
||||
const ledgerDocument = {
|
||||
nameId: 'smartmigration:smartmigration',
|
||||
data: {
|
||||
@@ -29,12 +93,10 @@ function createFakeDb(currentVersion: string) {
|
||||
},
|
||||
};
|
||||
|
||||
const emptyCollection = {
|
||||
find: () => ({
|
||||
async *[Symbol.asyncIterator]() {},
|
||||
}),
|
||||
updateMany: async () => ({ modifiedCount: 0 }),
|
||||
};
|
||||
const fakeCollections = new Map(
|
||||
Object.entries(collections).map(([name, documents]) => [name, createFakeCollection(documents)]),
|
||||
);
|
||||
const emptyCollection = createFakeCollection();
|
||||
|
||||
const ledgerCollection = {
|
||||
createIndex: async () => undefined,
|
||||
@@ -52,18 +114,69 @@ function createFakeDb(currentVersion: string) {
|
||||
return {
|
||||
mongoDb: {
|
||||
collection: (name: string) =>
|
||||
name === 'SmartdataEasyStore' ? ledgerCollection : emptyCollection,
|
||||
name === 'SmartdataEasyStore'
|
||||
? ledgerCollection
|
||||
: fakeCollections.get(name) || emptyCollection,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
tap.test('migration runner bridges old package-version targets without real schema steps', async () => {
|
||||
const runner = await createMigrationRunner(createFakeDb('13.16.0'), '13.31.0');
|
||||
tap.test('migration runner applies schema steps through the current target', async () => {
|
||||
const runner = await createMigrationRunner(createFakeDb('13.16.0'), '13.40.2');
|
||||
const result = await runner.run();
|
||||
|
||||
expect(result.currentVersionBefore).toEqual('13.16.0');
|
||||
expect(result.currentVersionAfter).toEqual('13.31.0');
|
||||
expect(result.currentVersionAfter).toEqual('13.40.2');
|
||||
expect(result.stepsApplied).toHaveLength(3);
|
||||
});
|
||||
|
||||
tap.test('migration runner rematerializes source-profile-backed route security', async () => {
|
||||
const profiles: Array<Record<string, any>> = [
|
||||
{
|
||||
_id: 'profile-doc-1',
|
||||
id: 'standard-profile',
|
||||
name: 'Standard',
|
||||
security: {
|
||||
ipAllowList: ['192.168.*', '127.0.0.1'],
|
||||
maxConnections: 1000,
|
||||
},
|
||||
},
|
||||
];
|
||||
const routes: Array<Record<string, any>> = [
|
||||
{
|
||||
_id: 'route-doc-1',
|
||||
id: 'route-1',
|
||||
route: {
|
||||
name: 'Public service domains',
|
||||
match: { ports: 443, domains: ['code.foss.global'] },
|
||||
action: { type: 'forward', targets: [{ host: '192.168.5.247', port: 443 }] },
|
||||
security: {
|
||||
ipAllowList: ['192.168.*', '*'],
|
||||
maxConnections: 1000,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
sourceProfileRef: 'standard-profile',
|
||||
sourceProfileName: 'Standard',
|
||||
},
|
||||
updatedAt: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const runner = await createMigrationRunner(
|
||||
createFakeDb('13.40.1', {
|
||||
SourceProfileDoc: profiles,
|
||||
RouteDoc: routes,
|
||||
}),
|
||||
'13.40.2',
|
||||
);
|
||||
const result = await runner.run();
|
||||
|
||||
expect(result.stepsApplied).toHaveLength(1);
|
||||
expect(routes[0].route.security.ipAllowList.includes('*')).toBeFalse();
|
||||
expect(routes[0].route.security.ipAllowList).toContain('192.168.*');
|
||||
expect(routes[0].route.security.maxConnections).toEqual(1000);
|
||||
expect(routes[0].metadata.lastResolvedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -91,7 +91,7 @@ tap.test('should resolve source profile onto a route', async () => {
|
||||
expect(result.metadata.lastResolvedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should merge inline route security with profile security', async () => {
|
||||
tap.test('should replace inline route security when source profile is selected', async () => {
|
||||
const route = makeRoute({
|
||||
security: {
|
||||
ipAllowList: ['127.0.0.1'],
|
||||
@@ -102,13 +102,26 @@ tap.test('should merge inline route security with profile security', async () =>
|
||||
|
||||
const result = resolver.resolveRoute(route, metadata);
|
||||
|
||||
// IP lists are unioned
|
||||
expect(result.route.security!.ipAllowList).toContain('192.168.0.0/16');
|
||||
expect(result.route.security!.ipAllowList).toContain('10.0.0.0/8');
|
||||
expect(result.route.security!.ipAllowList).toContain('127.0.0.1');
|
||||
expect(result.route.security!.ipAllowList!.includes('127.0.0.1')).toBeFalse();
|
||||
expect(result.route.security!.maxConnections).toEqual(1000);
|
||||
});
|
||||
|
||||
// Inline maxConnections overrides profile
|
||||
expect(result.route.security!.maxConnections).toEqual(5000);
|
||||
tap.test('should remove stale wildcard security from a profile-backed route', async () => {
|
||||
const route = makeRoute({
|
||||
security: {
|
||||
ipAllowList: ['*'],
|
||||
maxConnections: 5000,
|
||||
},
|
||||
});
|
||||
const metadata: IRouteMetadata = { sourceProfileRef: 'profile-1' };
|
||||
|
||||
const result = resolver.resolveRoute(route, metadata);
|
||||
|
||||
expect(result.route.security!.ipAllowList!.includes('*')).toBeFalse();
|
||||
expect(result.route.security!.ipAllowList).toContain('192.168.0.0/16');
|
||||
expect(result.route.security!.maxConnections).toEqual(1000);
|
||||
});
|
||||
|
||||
tap.test('should deduplicate IP lists during merge', async () => {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.40.1',
|
||||
version: '13.41.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
+178
-38
@@ -33,6 +33,7 @@ import { DnsManager } from './dns/manager.dns.js';
|
||||
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
||||
import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
|
||||
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
||||
import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig } from '../ts_interfaces/data/remoteingress.js';
|
||||
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
@@ -280,6 +281,9 @@ export class DcRouter {
|
||||
// Remote Ingress
|
||||
public remoteIngressManager?: RemoteIngressManager;
|
||||
public tunnelManager?: TunnelManager;
|
||||
private remoteIngressHubLifecycleChain: Promise<void> = Promise.resolve();
|
||||
private remoteIngressHubStopping = false;
|
||||
private remoteIngressHubGeneration = 0;
|
||||
|
||||
// VPN
|
||||
public vpnManager?: VpnManager;
|
||||
@@ -613,15 +617,10 @@ export class DcRouter {
|
||||
// Sync routes to RemoteIngressManager whenever routes change,
|
||||
// then push updated derived ports to the Rust hub binary
|
||||
async (routes) => {
|
||||
if (this.remoteIngressManager) {
|
||||
this.remoteIngressManager.setRoutes(routes as any[]);
|
||||
}
|
||||
if (this.tunnelManager) {
|
||||
try {
|
||||
await this.tunnelManager.syncAllowedEdges();
|
||||
} catch (err: unknown) {
|
||||
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
|
||||
}
|
||||
try {
|
||||
await this.updateRemoteIngressRoutes(routes as IDcRouterRouteConfig[]);
|
||||
} catch (err: unknown) {
|
||||
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
@@ -739,11 +738,7 @@ export class DcRouter {
|
||||
await this.setupRemoteIngress();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.tunnelManager) {
|
||||
await this.tunnelManager.stop();
|
||||
this.tunnelManager = undefined;
|
||||
}
|
||||
this.remoteIngressManager = undefined;
|
||||
await this.stopRemoteIngress();
|
||||
})
|
||||
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
||||
);
|
||||
@@ -1319,12 +1314,15 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
|
||||
if (this.remoteIngressManager) {
|
||||
(this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
|
||||
}
|
||||
if (this.tunnelManager) {
|
||||
await this.tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
await this.queueRemoteIngressHubTask(async () => {
|
||||
if (this.remoteIngressHubStopping) return;
|
||||
if (this.remoteIngressManager) {
|
||||
this.remoteIngressManager.setFirewallConfig(firewallConfig);
|
||||
}
|
||||
if (this.tunnelManager) {
|
||||
await this.tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private mergeSecurityPolicies(
|
||||
@@ -2340,28 +2338,180 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
logger.log('info', 'Setting up Remote Ingress hub...');
|
||||
this.remoteIngressHubStopping = false;
|
||||
const generation = ++this.remoteIngressHubGeneration;
|
||||
|
||||
// Initialize the edge registration manager
|
||||
this.remoteIngressManager = new RemoteIngressManager();
|
||||
await this.remoteIngressManager.initialize();
|
||||
this.remoteIngressManager.setFirewallConfig(
|
||||
await this.securityPolicyManager?.compileRemoteIngressFirewall(),
|
||||
);
|
||||
const remoteIngressManager = new RemoteIngressManager(this.options.remoteIngressConfig.performance);
|
||||
this.remoteIngressManager = remoteIngressManager;
|
||||
await remoteIngressManager.initialize();
|
||||
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
|
||||
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
||||
return;
|
||||
}
|
||||
remoteIngressManager.setFirewallConfig(firewallConfig);
|
||||
|
||||
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
||||
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
||||
// will push the complete merged routes here.
|
||||
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
|
||||
this.remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
|
||||
remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
|
||||
|
||||
// If ConfigManagers finished before us, re-apply routes
|
||||
// so the callback delivers the full DB set to our newly-created remoteIngressManager.
|
||||
if (this.routeConfigManager) {
|
||||
await this.routeConfigManager.applyRoutes();
|
||||
}
|
||||
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
||||
await this.queueRemoteIngressHubTask(async () => {
|
||||
await this.startRemoteIngressTunnelHubLocked(generation);
|
||||
});
|
||||
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const edgeCount = remoteIngressManager.getAllEdges().length;
|
||||
logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
|
||||
}
|
||||
|
||||
private isRemoteIngressHubGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
|
||||
return !this.remoteIngressHubStopping
|
||||
&& generation === this.remoteIngressHubGeneration
|
||||
&& this.remoteIngressManager === manager;
|
||||
}
|
||||
|
||||
private queueRemoteIngressHubTask<T>(task: () => Promise<T>): Promise<T> {
|
||||
const run = this.remoteIngressHubLifecycleChain.then(task);
|
||||
this.remoteIngressHubLifecycleChain = run.then(() => undefined, () => undefined);
|
||||
return run;
|
||||
}
|
||||
|
||||
private async stopRemoteIngress(): Promise<void> {
|
||||
this.remoteIngressHubStopping = true;
|
||||
this.remoteIngressHubGeneration++;
|
||||
await this.queueRemoteIngressHubTask(async () => {
|
||||
const currentTunnelManager = this.tunnelManager;
|
||||
this.tunnelManager = undefined;
|
||||
if (currentTunnelManager) {
|
||||
await currentTunnelManager.stop();
|
||||
}
|
||||
});
|
||||
this.remoteIngressManager = undefined;
|
||||
}
|
||||
|
||||
public async mutateRemoteIngressEdges<T>(
|
||||
mutation: (manager: RemoteIngressManager) => Promise<T>,
|
||||
syncAllowedEdges = true,
|
||||
): Promise<T> {
|
||||
return await this.queueRemoteIngressHubTask(async () => {
|
||||
if (this.remoteIngressHubStopping) {
|
||||
throw new Error('RemoteIngress is stopping');
|
||||
}
|
||||
const manager = this.remoteIngressManager;
|
||||
if (!manager) {
|
||||
throw new Error('RemoteIngress not configured');
|
||||
}
|
||||
const result = await mutation(manager);
|
||||
if (syncAllowedEdges && this.tunnelManager) {
|
||||
await this.tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private async updateRemoteIngressRoutes(routes: IDcRouterRouteConfig[]): Promise<void> {
|
||||
await this.queueRemoteIngressHubTask(async () => {
|
||||
if (this.remoteIngressHubStopping) return;
|
||||
if (this.remoteIngressManager) {
|
||||
this.remoteIngressManager.setRoutes(routes);
|
||||
}
|
||||
if (this.tunnelManager) {
|
||||
await this.tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async updateRemoteIngressHubSettings(
|
||||
updates: { performance?: IRemoteIngressPerformanceConfig },
|
||||
updatedBy: string,
|
||||
): Promise<IRemoteIngressHubSettings> {
|
||||
return await this.queueRemoteIngressHubTask(async () => {
|
||||
if (this.remoteIngressHubStopping) {
|
||||
throw new Error('RemoteIngress is stopping');
|
||||
}
|
||||
if (!this.remoteIngressManager) {
|
||||
throw new Error('RemoteIngress is not configured');
|
||||
}
|
||||
|
||||
const settings = await this.remoteIngressManager.updateHubSettings(updates, updatedBy);
|
||||
if (this.options.remoteIngressConfig?.enabled) {
|
||||
await this.restartRemoteIngressTunnelHubLocked();
|
||||
}
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
|
||||
private async restartRemoteIngressTunnelHubLocked(): Promise<void> {
|
||||
const generation = ++this.remoteIngressHubGeneration;
|
||||
if (!this.remoteIngressManager || !this.options.remoteIngressConfig?.enabled || this.remoteIngressHubStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTunnelManager = this.tunnelManager;
|
||||
this.tunnelManager = undefined;
|
||||
if (currentTunnelManager) {
|
||||
await currentTunnelManager.stop();
|
||||
}
|
||||
|
||||
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
|
||||
return;
|
||||
}
|
||||
await this.startRemoteIngressTunnelHubLocked(generation);
|
||||
}
|
||||
|
||||
private async startRemoteIngressTunnelHubLocked(generation: number): Promise<void> {
|
||||
const riCfg = this.options.remoteIngressConfig;
|
||||
const manager = this.remoteIngressManager;
|
||||
if (!riCfg?.enabled || !manager || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tlsConfig = await this.resolveRemoteIngressTlsConfig(riCfg);
|
||||
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tunnelManager = new TunnelManager(manager, {
|
||||
tunnelPort: riCfg.tunnelPort ?? 8443,
|
||||
targetHost: '127.0.0.1',
|
||||
tls: tlsConfig,
|
||||
performance: manager.getHubPerformanceConfig(),
|
||||
});
|
||||
try {
|
||||
await tunnelManager.start();
|
||||
} catch (err) {
|
||||
await tunnelManager.stop().catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
|
||||
await tunnelManager.stop();
|
||||
return;
|
||||
}
|
||||
this.tunnelManager = tunnelManager;
|
||||
}
|
||||
|
||||
private async resolveRemoteIngressTlsConfig(
|
||||
riCfg: NonNullable<IDcRouterOptions['remoteIngressConfig']>,
|
||||
): Promise<{ certPem: string; keyPem: string } | undefined> {
|
||||
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
||||
let tlsConfig: { certPem: string; keyPem: string } | undefined;
|
||||
|
||||
// Priority 1: Explicit cert/key file paths
|
||||
@@ -2391,17 +2541,7 @@ export class DcRouter {
|
||||
logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
|
||||
}
|
||||
|
||||
// Create and start the tunnel manager
|
||||
this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
|
||||
tunnelPort: riCfg.tunnelPort ?? 8443,
|
||||
targetHost: '127.0.0.1',
|
||||
tls: tlsConfig,
|
||||
performance: riCfg.performance,
|
||||
});
|
||||
await this.tunnelManager.start();
|
||||
|
||||
const edgeCount = this.remoteIngressManager.getAllEdges().length;
|
||||
logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
|
||||
return tlsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -281,6 +281,7 @@ export class ReferenceResolver {
|
||||
/**
|
||||
* Resolve references for a single route.
|
||||
* Materializes source profile and/or network target into the route's fields.
|
||||
* When a source profile is selected, it owns the route security fully.
|
||||
* Returns the resolved route and updated metadata.
|
||||
*/
|
||||
public resolveRoute(
|
||||
@@ -293,10 +294,9 @@ export class ReferenceResolver {
|
||||
const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
|
||||
if (resolvedSecurity) {
|
||||
const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
|
||||
// Merge: profile provides base, route's inline values override
|
||||
route = {
|
||||
...route,
|
||||
security: this.mergeSecurityFields(resolvedSecurity, route.security),
|
||||
security: this.cloneSecurityFields(resolvedSecurity),
|
||||
};
|
||||
resolvedMetadata.sourceProfileName = profile?.name;
|
||||
resolvedMetadata.lastResolvedAt = Date.now();
|
||||
@@ -445,10 +445,15 @@ export class ReferenceResolver {
|
||||
if (override.authentication !== undefined) merged.authentication = override.authentication;
|
||||
if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
|
||||
if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
|
||||
if (override.vpn !== undefined) merged.vpn = override.vpn;
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private cloneSecurityFields(security: IRouteSecurity): IRouteSecurity {
|
||||
return structuredClone(security);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private: persistence
|
||||
// =========================================================================
|
||||
|
||||
@@ -175,6 +175,8 @@ export class RouteConfigManager {
|
||||
return { success: false, message: 'Route not found' };
|
||||
}
|
||||
|
||||
const previousSourceProfileRef = stored.metadata?.sourceProfileRef;
|
||||
|
||||
const isToggleOnlyPatch = patch.enabled !== undefined
|
||||
&& patch.route === undefined
|
||||
&& patch.metadata === undefined;
|
||||
@@ -216,6 +218,13 @@ export class RouteConfigManager {
|
||||
...stored.metadata,
|
||||
...patch.metadata,
|
||||
});
|
||||
if (
|
||||
previousSourceProfileRef
|
||||
&& !stored.metadata?.sourceProfileRef
|
||||
&& !patch.route?.security
|
||||
) {
|
||||
delete stored.route.security;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-resolve if metadata refs exist and resolver is available
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||
import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
|
||||
|
||||
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class RemoteIngressHubSettingsDoc extends plugins.smartdata.SmartDataDbDoc<RemoteIngressHubSettingsDoc, RemoteIngressHubSettingsDoc> {
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public settingsId: string = 'remote-ingress-hub-settings';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public performance?: IRemoteIngressPerformanceConfig;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedAt: number = 0;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedBy: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static async load(): Promise<RemoteIngressHubSettingsDoc | null> {
|
||||
return await RemoteIngressHubSettingsDoc.getInstance({ settingsId: 'remote-ingress-hub-settings' });
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export * from './classes.cert-backoff.doc.js';
|
||||
|
||||
// Remote ingress document classes
|
||||
export * from './classes.remote-ingress-edge.doc.js';
|
||||
export * from './classes.remote-ingress-hub-settings.doc.js';
|
||||
|
||||
// RADIUS document classes
|
||||
export * from './classes.vlan-mappings.doc.js';
|
||||
|
||||
@@ -208,7 +208,7 @@ export class ConfigHandler {
|
||||
hubDomain: riCfg?.hubDomain || null,
|
||||
tlsMode,
|
||||
connectedEdgeIps,
|
||||
performance: riCfg?.performance,
|
||||
performance: dcRouter.remoteIngressManager?.getHubPerformanceConfig() || riCfg?.performance,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -52,30 +52,21 @@ export class RemoteIngressHandler {
|
||||
scope: 'remote-ingress:write',
|
||||
requireAdminIdentity: true,
|
||||
});
|
||||
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||
|
||||
if (!manager) {
|
||||
try {
|
||||
const edge = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges((manager) => manager.createEdge(
|
||||
dataArg.name,
|
||||
dataArg.listenPorts || [],
|
||||
dataArg.tags,
|
||||
dataArg.autoDerivePorts ?? true,
|
||||
dataArg.performance,
|
||||
));
|
||||
return { success: true, edge };
|
||||
} catch (err: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
edge: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
const edge = await manager.createEdge(
|
||||
dataArg.name,
|
||||
dataArg.listenPorts || [],
|
||||
dataArg.tags,
|
||||
dataArg.autoDerivePorts ?? true,
|
||||
dataArg.performance,
|
||||
);
|
||||
|
||||
// Sync allowed edges with the hub
|
||||
if (tunnelManager) {
|
||||
await tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
|
||||
return { success: true, edge };
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -89,21 +80,18 @@ export class RemoteIngressHandler {
|
||||
scope: 'remote-ingress:write',
|
||||
requireAdminIdentity: true,
|
||||
});
|
||||
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||
|
||||
if (!manager) {
|
||||
return { success: false, message: 'RemoteIngress not configured' };
|
||||
}
|
||||
|
||||
const deleted = await manager.deleteEdge(dataArg.id);
|
||||
if (deleted && tunnelManager) {
|
||||
await tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
const deleted = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
|
||||
(manager) => manager.deleteEdge(dataArg.id),
|
||||
).catch((err: unknown) => {
|
||||
if ((err as Error).message.includes('RemoteIngress')) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return {
|
||||
success: deleted,
|
||||
message: deleted ? undefined : 'Edge not found',
|
||||
message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
|
||||
};
|
||||
},
|
||||
),
|
||||
@@ -118,42 +106,42 @@ export class RemoteIngressHandler {
|
||||
scope: 'remote-ingress:write',
|
||||
requireAdminIdentity: true,
|
||||
});
|
||||
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||
const result = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(async (manager) => {
|
||||
const edge = await manager.updateEdge(dataArg.id, {
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
enabled: dataArg.enabled,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
if (!manager) {
|
||||
return { success: false, edge: null as any };
|
||||
}
|
||||
if (!edge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const edge = await manager.updateEdge(dataArg.id, {
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
enabled: dataArg.enabled,
|
||||
performance: dataArg.performance,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
if (!edge) {
|
||||
return { success: false, edge: null as any };
|
||||
}
|
||||
|
||||
// Sync allowed edges — ports, tags, or enabled may have changed
|
||||
if (tunnelManager) {
|
||||
await tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
|
||||
const breakdown = manager.getPortBreakdown(edge);
|
||||
return {
|
||||
success: true,
|
||||
edge: {
|
||||
const breakdown = manager.getPortBreakdown(edge);
|
||||
return {
|
||||
...edge,
|
||||
secret: '********',
|
||||
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
||||
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
|
||||
manualPorts: breakdown.manual,
|
||||
derivedPorts: breakdown.derived,
|
||||
},
|
||||
};
|
||||
}).catch((err: unknown) => {
|
||||
if ((err as Error).message.includes('RemoteIngress')) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return { success: false, edge: null as any };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
edge: result,
|
||||
};
|
||||
},
|
||||
),
|
||||
@@ -168,23 +156,18 @@ export class RemoteIngressHandler {
|
||||
scope: 'remote-ingress:write',
|
||||
requireAdminIdentity: true,
|
||||
});
|
||||
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||
|
||||
if (!manager) {
|
||||
return { success: false, secret: '' };
|
||||
}
|
||||
|
||||
const secret = await manager.regenerateSecret(dataArg.id);
|
||||
const secret = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
|
||||
(manager) => manager.regenerateSecret(dataArg.id),
|
||||
).catch((err: unknown) => {
|
||||
if ((err as Error).message.includes('RemoteIngress')) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!secret) {
|
||||
return { success: false, secret: '' };
|
||||
}
|
||||
|
||||
// Sync allowed edges since secret changed
|
||||
if (tunnelManager) {
|
||||
await tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
|
||||
return { success: true, secret };
|
||||
},
|
||||
),
|
||||
@@ -205,6 +188,46 @@ export class RemoteIngressHandler {
|
||||
),
|
||||
);
|
||||
|
||||
// Get hub-level settings (read)
|
||||
viewRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressHubSettings>(
|
||||
'getRemoteIngressHubSettings',
|
||||
async (dataArg, toolsArg) => {
|
||||
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
|
||||
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||
return {
|
||||
settings: manager?.getHubSettings() || {
|
||||
updatedAt: 0,
|
||||
updatedBy: 'default',
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Update hub-level settings (write)
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngressHubSettings>(
|
||||
'updateRemoteIngressHubSettings',
|
||||
async (dataArg, toolsArg) => {
|
||||
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
|
||||
scope: 'remote-ingress:write',
|
||||
requireAdminIdentity: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const settings = await this.opsServerRef.dcRouterRef.updateRemoteIngressHubSettings(
|
||||
{ performance: dataArg.performance },
|
||||
auth.userId,
|
||||
);
|
||||
return { success: true, settings };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get a connection token for an edge (write — exposes secret)
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
||||
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
|
||||
import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
|
||||
|
||||
interface IRemoteIngressFirewallConfig {
|
||||
blockedIps?: string[];
|
||||
}
|
||||
|
||||
type TPerformanceIntegerField =
|
||||
| 'maxStreamsPerEdge'
|
||||
| 'totalWindowBudgetBytes'
|
||||
| 'minStreamWindowBytes'
|
||||
| 'maxStreamWindowBytes'
|
||||
| 'sustainedStreamWindowBytes'
|
||||
| 'quicDatagramReceiveBufferBytes'
|
||||
| 'streamFramePayloadBytes'
|
||||
| 'firstDataConnectTimeoutMs'
|
||||
| 'clientWriteTimeoutMs';
|
||||
|
||||
const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
|
||||
maxStreamsPerEdge: 100_000,
|
||||
totalWindowBudgetBytes: 1_073_741_824,
|
||||
minStreamWindowBytes: 16_777_216,
|
||||
maxStreamWindowBytes: 134_217_728,
|
||||
sustainedStreamWindowBytes: 134_217_728,
|
||||
quicDatagramReceiveBufferBytes: 67_108_864,
|
||||
streamFramePayloadBytes: 16_777_216,
|
||||
firstDataConnectTimeoutMs: 3_600_000,
|
||||
clientWriteTimeoutMs: 3_600_000,
|
||||
};
|
||||
|
||||
const maxServerFirstPorts = 128;
|
||||
|
||||
function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
|
||||
const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
|
||||
return [...ports].sort((a, b) => a - b);
|
||||
@@ -20,8 +45,12 @@ export class RemoteIngressManager {
|
||||
private edges: Map<string, IRemoteIngress> = new Map();
|
||||
private routes: IDcRouterRouteConfig[] = [];
|
||||
private firewallConfig?: IRemoteIngressFirewallConfig;
|
||||
private hubSettings: IRemoteIngressHubSettings = {
|
||||
updatedAt: 0,
|
||||
updatedBy: 'default',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(private seedHubPerformance?: IRemoteIngressPerformanceConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +79,28 @@ export class RemoteIngressManager {
|
||||
};
|
||||
this.edges.set(edge.id, edge);
|
||||
}
|
||||
|
||||
await this.initializeHubSettings();
|
||||
}
|
||||
|
||||
private async initializeHubSettings(): Promise<void> {
|
||||
let doc = await RemoteIngressHubSettingsDoc.load();
|
||||
if (!doc) {
|
||||
const seedPerformance = this.normalizePerformanceConfig(this.seedHubPerformance);
|
||||
if (seedPerformance) {
|
||||
doc = new RemoteIngressHubSettingsDoc();
|
||||
doc.settingsId = 'remote-ingress-hub-settings';
|
||||
doc.performance = seedPerformance;
|
||||
doc.updatedAt = Date.now();
|
||||
doc.updatedBy = 'seed';
|
||||
await doc.save();
|
||||
}
|
||||
}
|
||||
|
||||
this.hubSettings = doc ? this.toHubSettings(doc) : {
|
||||
updatedAt: 0,
|
||||
updatedBy: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +117,38 @@ export class RemoteIngressManager {
|
||||
this.firewallConfig = firewallConfig;
|
||||
}
|
||||
|
||||
public getHubSettings(): IRemoteIngressHubSettings {
|
||||
return {
|
||||
...this.hubSettings,
|
||||
performance: this.hubSettings.performance ? { ...this.hubSettings.performance } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public getHubPerformanceConfig(): IRemoteIngressPerformanceConfig | undefined {
|
||||
return this.hubSettings.performance && Object.keys(this.hubSettings.performance).length > 0
|
||||
? { ...this.hubSettings.performance }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public async updateHubSettings(
|
||||
updates: { performance?: IRemoteIngressPerformanceConfig },
|
||||
updatedBy: string,
|
||||
): Promise<IRemoteIngressHubSettings> {
|
||||
let doc = await RemoteIngressHubSettingsDoc.load();
|
||||
if (!doc) {
|
||||
doc = new RemoteIngressHubSettingsDoc();
|
||||
doc.settingsId = 'remote-ingress-hub-settings';
|
||||
}
|
||||
|
||||
doc.performance = this.normalizePerformanceConfig(updates.performance);
|
||||
doc.updatedAt = Date.now();
|
||||
doc.updatedBy = updatedBy;
|
||||
await doc.save();
|
||||
|
||||
this.hubSettings = this.toHubSettings(doc);
|
||||
return this.getHubSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
|
||||
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
|
||||
@@ -324,4 +407,90 @@ export class RemoteIngressManager {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private normalizePerformanceConfig(
|
||||
performance?: IRemoteIngressPerformanceConfig,
|
||||
): IRemoteIngressPerformanceConfig | undefined {
|
||||
if (!performance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const next: IRemoteIngressPerformanceConfig = {};
|
||||
const validProfiles: TRemoteIngressPerformanceProfile[] = ['balanced', 'throughput', 'highConcurrency'];
|
||||
if (performance.profile !== undefined) {
|
||||
if (!validProfiles.includes(performance.profile)) {
|
||||
throw new Error('Invalid RemoteIngress performance profile');
|
||||
}
|
||||
next.profile = performance.profile;
|
||||
}
|
||||
|
||||
const assignPositiveInteger = (field: TPerformanceIntegerField) => {
|
||||
const value = performance[field];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
const maxValue = performanceIntegerMaxByField[field];
|
||||
if (!Number.isSafeInteger(value) || value < 1 || value > maxValue) {
|
||||
throw new Error(`${field} must be a positive safe integer no greater than ${maxValue}`);
|
||||
}
|
||||
(next as Record<string, number>)[field] = value;
|
||||
};
|
||||
|
||||
assignPositiveInteger('maxStreamsPerEdge');
|
||||
assignPositiveInteger('totalWindowBudgetBytes');
|
||||
assignPositiveInteger('minStreamWindowBytes');
|
||||
assignPositiveInteger('maxStreamWindowBytes');
|
||||
assignPositiveInteger('sustainedStreamWindowBytes');
|
||||
assignPositiveInteger('quicDatagramReceiveBufferBytes');
|
||||
assignPositiveInteger('streamFramePayloadBytes');
|
||||
assignPositiveInteger('firstDataConnectTimeoutMs');
|
||||
assignPositiveInteger('clientWriteTimeoutMs');
|
||||
|
||||
if (
|
||||
next.minStreamWindowBytes !== undefined
|
||||
&& next.maxStreamWindowBytes !== undefined
|
||||
&& next.minStreamWindowBytes > next.maxStreamWindowBytes
|
||||
) {
|
||||
throw new Error('minStreamWindowBytes must not exceed maxStreamWindowBytes');
|
||||
}
|
||||
if (
|
||||
next.sustainedStreamWindowBytes !== undefined
|
||||
&& next.maxStreamWindowBytes !== undefined
|
||||
&& next.sustainedStreamWindowBytes > next.maxStreamWindowBytes
|
||||
) {
|
||||
throw new Error('sustainedStreamWindowBytes must not exceed maxStreamWindowBytes');
|
||||
}
|
||||
|
||||
const configuredServerFirstPorts = performance.serverFirstPorts;
|
||||
if (configuredServerFirstPorts !== undefined) {
|
||||
if (!Array.isArray(configuredServerFirstPorts)) {
|
||||
throw new Error('serverFirstPorts must contain valid port numbers');
|
||||
}
|
||||
if (configuredServerFirstPorts.length > maxServerFirstPorts) {
|
||||
throw new Error(`serverFirstPorts must contain at most ${maxServerFirstPorts} ports`);
|
||||
}
|
||||
const serverFirstPorts = [...new Set(configuredServerFirstPorts.map((port) => Number(port)))].sort((a, b) => a - b);
|
||||
for (const port of serverFirstPorts) {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error('serverFirstPorts must contain valid port numbers');
|
||||
}
|
||||
if (port === 443) {
|
||||
throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
|
||||
}
|
||||
}
|
||||
if (serverFirstPorts.length > 0) {
|
||||
next.serverFirstPorts = serverFirstPorts;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
private toHubSettings(doc: RemoteIngressHubSettingsDoc): IRemoteIngressHubSettings {
|
||||
return {
|
||||
performance: doc.performance,
|
||||
updatedAt: doc.updatedAt,
|
||||
updatedBy: doc.updatedBy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export class TunnelManager {
|
||||
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
||||
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private syncChain: Promise<void> = Promise.resolve();
|
||||
private reconcileChain: Promise<void> = Promise.resolve();
|
||||
private stopped = true;
|
||||
|
||||
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
||||
this.manager = manager;
|
||||
@@ -64,30 +66,51 @@ export class TunnelManager {
|
||||
* Start the tunnel hub and load allowed edges.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
await this.hub.start({
|
||||
tunnelPort: this.config.tunnelPort ?? 8443,
|
||||
targetHost: this.config.targetHost ?? '127.0.0.1',
|
||||
tls: this.config.tls,
|
||||
...(this.config.performance ? { performance: this.config.performance } : {}),
|
||||
} as any);
|
||||
this.stopped = false;
|
||||
try {
|
||||
await this.hub.start({
|
||||
tunnelPort: this.config.tunnelPort ?? 8443,
|
||||
targetHost: this.config.targetHost ?? '127.0.0.1',
|
||||
tls: this.config.tls,
|
||||
...(this.config.performance ? { performance: this.config.performance } : {}),
|
||||
} as any);
|
||||
|
||||
// Send allowed edges to the hub
|
||||
await this.syncAllowedEdges();
|
||||
if (this.stopped) return;
|
||||
|
||||
// Periodically reconcile with authoritative Rust hub status
|
||||
this.reconcileInterval = setInterval(() => {
|
||||
this.reconcile().catch(() => {});
|
||||
}, 15_000);
|
||||
// Send allowed edges to the hub
|
||||
await this.syncAllowedEdges();
|
||||
|
||||
if (this.stopped) return;
|
||||
|
||||
// Periodically reconcile with authoritative Rust hub status
|
||||
this.reconcileInterval = setInterval(() => {
|
||||
this.reconcileChain = this.reconcileChain
|
||||
.catch(() => {})
|
||||
.then(() => this.reconcile());
|
||||
this.reconcileChain.catch(() => {});
|
||||
}, 15_000);
|
||||
} catch (err) {
|
||||
await this.stop();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the tunnel hub.
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
this.stopped = true;
|
||||
if (this.reconcileInterval) {
|
||||
clearInterval(this.reconcileInterval);
|
||||
this.reconcileInterval = null;
|
||||
}
|
||||
await Promise.all([
|
||||
this.syncChain.catch(() => {}),
|
||||
this.reconcileChain.catch(() => {}),
|
||||
]);
|
||||
// Remove event listeners before stopping to prevent leaks
|
||||
this.hub.removeAllListeners();
|
||||
await this.hub.stop();
|
||||
@@ -99,7 +122,9 @@ export class TunnelManager {
|
||||
* Overwrites event-derived activeTunnels with the real activeStreams count.
|
||||
*/
|
||||
private async reconcile(): Promise<void> {
|
||||
if (this.stopped) return;
|
||||
const hubStatus = await this.hub.getStatus();
|
||||
if (this.stopped) return;
|
||||
if (!hubStatus || !hubStatus.connectedEdges) return;
|
||||
|
||||
const rustEdgeIds = new Set<string>();
|
||||
@@ -144,7 +169,9 @@ export class TunnelManager {
|
||||
*/
|
||||
public async syncAllowedEdges(): Promise<void> {
|
||||
const run = this.syncChain.catch(() => {}).then(async () => {
|
||||
if (this.stopped) return;
|
||||
const edges = this.manager.getAllowedEdges();
|
||||
if (this.stopped) return;
|
||||
await this.hub.updateAllowedEdges(edges as any);
|
||||
});
|
||||
this.syncChain = run;
|
||||
|
||||
@@ -63,6 +63,12 @@ export interface IRemoteIngressPerformanceConfig {
|
||||
serverFirstPorts?: number[];
|
||||
}
|
||||
|
||||
export interface IRemoteIngressHubSettings {
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
updatedAt: number;
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
export interface IRemoteIngressPerformanceEffective {
|
||||
profile: TRemoteIngressPerformanceProfile;
|
||||
maxStreamsPerEdge: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
import type { IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress Edge Management
|
||||
@@ -147,3 +147,40 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hub-level RemoteIngress settings.
|
||||
*/
|
||||
export interface IReq_GetRemoteIngressHubSettings extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetRemoteIngressHubSettings
|
||||
> {
|
||||
method: 'getRemoteIngressHubSettings';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
apiToken?: string;
|
||||
};
|
||||
response: {
|
||||
settings: IRemoteIngressHubSettings;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update hub-level RemoteIngress settings.
|
||||
*/
|
||||
export interface IReq_UpdateRemoteIngressHubSettings extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_UpdateRemoteIngressHubSettings
|
||||
> {
|
||||
method: 'updateRemoteIngressHubSettings';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
apiToken?: string;
|
||||
performance?: IRemoteIngressPerformanceConfig;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
settings?: IRemoteIngressHubSettings;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,131 @@ export interface IMigrationRunner {
|
||||
run(): Promise<IMigrationRunResult>;
|
||||
}
|
||||
|
||||
type TMigrationSecurity = Record<string, any>;
|
||||
|
||||
function mergeMigrationSecurityFields(
|
||||
base: TMigrationSecurity | undefined,
|
||||
override: TMigrationSecurity | undefined,
|
||||
): TMigrationSecurity {
|
||||
if (!base && !override) return {};
|
||||
if (!base) return structuredClone(override || {});
|
||||
if (!override) return structuredClone(base || {});
|
||||
|
||||
const merged: TMigrationSecurity = structuredClone(base);
|
||||
|
||||
if (override.ipAllowList || base.ipAllowList) {
|
||||
merged.ipAllowList = [
|
||||
...new Set([
|
||||
...(base.ipAllowList || []),
|
||||
...(override.ipAllowList || []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
if (override.ipBlockList || base.ipBlockList) {
|
||||
merged.ipBlockList = [
|
||||
...new Set([
|
||||
...(base.ipBlockList || []),
|
||||
...(override.ipBlockList || []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
for (const key of ['maxConnections', 'rateLimit', 'authentication', 'basicAuth', 'jwtAuth', 'vpn']) {
|
||||
if (override[key] !== undefined) {
|
||||
merged[key] = structuredClone(override[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveMigrationSourceProfileSecurity(
|
||||
profileId: string,
|
||||
profiles: Map<string, any>,
|
||||
visited = new Set<string>(),
|
||||
depth = 0,
|
||||
): TMigrationSecurity | null {
|
||||
if (depth > 5 || visited.has(profileId)) return null;
|
||||
|
||||
const profile = profiles.get(profileId);
|
||||
if (!profile) return null;
|
||||
|
||||
visited.add(profileId);
|
||||
let baseSecurity: TMigrationSecurity = {};
|
||||
const extendsProfiles = Array.isArray(profile.extendsProfiles) ? profile.extendsProfiles : [];
|
||||
for (const parentId of extendsProfiles) {
|
||||
if (typeof parentId !== 'string') continue;
|
||||
const parentSecurity = resolveMigrationSourceProfileSecurity(
|
||||
parentId,
|
||||
profiles,
|
||||
new Set(visited),
|
||||
depth + 1,
|
||||
);
|
||||
if (parentSecurity) {
|
||||
baseSecurity = mergeMigrationSecurityFields(baseSecurity, parentSecurity);
|
||||
}
|
||||
}
|
||||
|
||||
return mergeMigrationSecurityFields(baseSecurity, profile.security || {});
|
||||
}
|
||||
|
||||
async function rematerializeSourceProfileRouteSecurity(ctx: {
|
||||
mongo?: { collection: (name: string) => any };
|
||||
log: { log: (level: 'info', message: string) => void };
|
||||
}): Promise<void> {
|
||||
const profileCollection = ctx.mongo!.collection('SourceProfileDoc');
|
||||
const routeCollection = ctx.mongo!.collection('RouteDoc');
|
||||
const profiles = new Map<string, any>();
|
||||
|
||||
for await (const profile of profileCollection.find({})) {
|
||||
if (typeof (profile as any).id === 'string') {
|
||||
profiles.set((profile as any).id, profile);
|
||||
}
|
||||
}
|
||||
|
||||
let inspected = 0;
|
||||
let migrated = 0;
|
||||
let skippedMissingProfile = 0;
|
||||
const now = Date.now();
|
||||
|
||||
for await (const routeDoc of routeCollection.find({})) {
|
||||
const sourceProfileRef = (routeDoc as any).metadata?.sourceProfileRef;
|
||||
if (typeof sourceProfileRef !== 'string' || sourceProfileRef.trim() === '') continue;
|
||||
inspected++;
|
||||
|
||||
const resolvedSecurity = resolveMigrationSourceProfileSecurity(sourceProfileRef, profiles);
|
||||
const profile = profiles.get(sourceProfileRef);
|
||||
if (!resolvedSecurity || !profile) {
|
||||
skippedMissingProfile++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentSecurity = (routeDoc as any).route?.security || {};
|
||||
const securityChanged = JSON.stringify(currentSecurity) !== JSON.stringify(resolvedSecurity);
|
||||
const profileNameChanged = (routeDoc as any).metadata?.sourceProfileName !== profile.name;
|
||||
if (!securityChanged && !profileNameChanged) continue;
|
||||
|
||||
const query = (routeDoc as any)._id
|
||||
? { _id: (routeDoc as any)._id }
|
||||
: { id: (routeDoc as any).id };
|
||||
await routeCollection.updateOne(query, {
|
||||
$set: {
|
||||
'route.security': structuredClone(resolvedSecurity),
|
||||
'metadata.sourceProfileName': profile.name,
|
||||
'metadata.lastResolvedAt': now,
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
migrated++;
|
||||
}
|
||||
|
||||
ctx.log.log(
|
||||
'info',
|
||||
`rematerialize-source-profile-route-security: migrated ${migrated}/${inspected} route(s), skipped ${skippedMissingProfile} missing profile ref(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function migrateTargetProfileTargetHosts(ctx: {
|
||||
mongo?: { collection: (name: string) => any };
|
||||
log: { log: (level: 'info', message: string) => void };
|
||||
@@ -167,6 +292,12 @@ export async function createMigrationRunner(
|
||||
.description('Backfill RouteDoc.systemKey for persisted config/email/dns routes')
|
||||
.up(async (ctx) => {
|
||||
await backfillSystemRouteKeys(ctx);
|
||||
})
|
||||
.step('rematerialize-source-profile-route-security')
|
||||
.from('13.18.0').to('13.40.2')
|
||||
.description('Replace stale route security with resolved source profile security')
|
||||
.up(async (ctx) => {
|
||||
await rematerializeSourceProfileRouteSecurity(ctx);
|
||||
});
|
||||
|
||||
return migration;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.40.1',
|
||||
version: '13.41.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
+41
-1
@@ -260,6 +260,7 @@ export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>
|
||||
export interface IRemoteIngressState {
|
||||
edges: interfaces.data.IRemoteIngress[];
|
||||
statuses: interfaces.data.IRemoteIngressStatus[];
|
||||
hubSettings: interfaces.data.IRemoteIngressHubSettings | null;
|
||||
selectedEdgeId: string | null;
|
||||
newEdgeId: string | null;
|
||||
isLoading: boolean;
|
||||
@@ -272,6 +273,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
|
||||
{
|
||||
edges: [],
|
||||
statuses: [],
|
||||
hubSettings: null,
|
||||
selectedEdgeId: null,
|
||||
newEdgeId: null,
|
||||
isLoading: false,
|
||||
@@ -1094,15 +1096,21 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
||||
interfaces.requests.IReq_GetRemoteIngressStatus
|
||||
>('/typedrequest', 'getRemoteIngressStatus');
|
||||
|
||||
const [edgesResponse, statusResponse] = await Promise.all([
|
||||
const hubSettingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRemoteIngressHubSettings
|
||||
>('/typedrequest', 'getRemoteIngressHubSettings');
|
||||
|
||||
const [edgesResponse, statusResponse, hubSettingsResponse] = await Promise.all([
|
||||
edgesRequest.fire({ identity: context.identity }),
|
||||
statusRequest.fire({ identity: context.identity }),
|
||||
hubSettingsRequest.fire({ identity: context.identity }),
|
||||
]);
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
edges: edgesResponse.edges,
|
||||
statuses: statusResponse.statuses,
|
||||
hubSettings: hubSettingsResponse.settings,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
@@ -1219,6 +1227,38 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.createAction<{
|
||||
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateRemoteIngressHubSettings
|
||||
>('/typedrequest', 'updateRemoteIngressHubSettings');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity!,
|
||||
performance: dataArg.performance,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
return {
|
||||
...currentState,
|
||||
error: response.message || 'Failed to update RemoteIngress hub settings',
|
||||
};
|
||||
}
|
||||
|
||||
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to update RemoteIngress hub settings',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -12,6 +12,17 @@ import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from '../shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
const performanceProfileOptions = [
|
||||
{ key: '', option: 'Default' },
|
||||
{ key: 'balanced', option: 'Balanced' },
|
||||
{ key: 'throughput', option: 'Throughput' },
|
||||
{ key: 'highConcurrency', option: 'High concurrency' },
|
||||
];
|
||||
|
||||
function getDropdownKey(value: any): string {
|
||||
return typeof value === 'string' ? value : value?.key || '';
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-remoteingress': OpsViewRemoteIngress;
|
||||
@@ -137,6 +148,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
.metricMuted {
|
||||
color: var(--text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.settingsNote {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -308,6 +326,14 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Hub Settings',
|
||||
iconName: 'lucide:slidersHorizontal',
|
||||
type: ['header' as const],
|
||||
actionFunc: async () => {
|
||||
await this.showHubSettingsDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Enable',
|
||||
iconName: 'lucide:play',
|
||||
@@ -591,4 +617,142 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
|
||||
return base ? {} : undefined;
|
||||
}
|
||||
|
||||
private async showHubSettingsDialog(): Promise<void> {
|
||||
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
||||
const performance = this.riState.hubSettings?.performance || {};
|
||||
const selectedProfile = performanceProfileOptions.find((option) => option.key === (performance.profile || '')) || performanceProfileOptions[0];
|
||||
const updatedAt = this.riState.hubSettings?.updatedAt
|
||||
? new Date(this.riState.hubSettings.updatedAt).toLocaleString()
|
||||
: 'not persisted yet';
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'RemoteIngress Hub Settings',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'profile'}
|
||||
.label=${'Performance Profile'}
|
||||
.options=${performanceProfileOptions}
|
||||
.selectedOption=${selectedProfile}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'maxStreamsPerEdge'}
|
||||
.label=${'Max Connections / Edge'}
|
||||
.description=${'Maximum concurrent client streams per edge. Leave empty for RemoteIngress defaults.'}
|
||||
.value=${performance.maxStreamsPerEdge?.toString() || ''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'clientWriteTimeoutMs'}
|
||||
.label=${'Client Write Timeout'}
|
||||
.description=${'Milliseconds before idle client writes are timed out. Leave empty for default.'}
|
||||
.value=${performance.clientWriteTimeoutMs?.toString() || ''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'firstDataConnectTimeoutMs'}
|
||||
.label=${'First Data Timeout'}
|
||||
.description=${'Milliseconds to wait for initial client data before connecting upstream. Leave empty for default.'}
|
||||
.value=${performance.firstDataConnectTimeoutMs?.toString() || ''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'serverFirstPorts'}
|
||||
.label=${'Server-first Ports'}
|
||||
.description=${'Comma-separated ports such as 21, 22, 25, 110, 143, 587. Do not include 443.'}
|
||||
.value=${(performance.serverFirstPorts || []).join(', ')}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
<p class="settingsNote">
|
||||
Saving restarts the RemoteIngress hub so connected edges reconnect and pick up the new defaults.
|
||||
Last updated: ${updatedAt} by ${this.riState.hubSettings?.updatedBy || 'default'}.
|
||||
</p>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'lucide:check',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
let performanceSettings: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
||||
try {
|
||||
performanceSettings = this.collectHubPerformanceSettings(formData);
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.updateRemoteIngressHubSettingsAction,
|
||||
{ performance: performanceSettings },
|
||||
);
|
||||
if (nextState.error) {
|
||||
DeesToast.show({ message: nextState.error, type: 'error', duration: 4000 });
|
||||
return;
|
||||
}
|
||||
await modalArg.destroy();
|
||||
DeesToast.show({ message: 'RemoteIngress hub settings saved', type: 'success', duration: 3000 });
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private collectHubPerformanceSettings(formData: Record<string, any>): interfaces.data.IRemoteIngressPerformanceConfig | undefined {
|
||||
const next: interfaces.data.IRemoteIngressPerformanceConfig = {};
|
||||
const profile = getDropdownKey(formData.profile) as interfaces.data.TRemoteIngressPerformanceProfile | '';
|
||||
if (profile) {
|
||||
next.profile = profile;
|
||||
}
|
||||
|
||||
this.assignPositiveIntegerSetting(next, 'maxStreamsPerEdge', formData.maxStreamsPerEdge, 'Max Connections / Edge');
|
||||
this.assignPositiveIntegerSetting(next, 'clientWriteTimeoutMs', formData.clientWriteTimeoutMs, 'Client Write Timeout');
|
||||
this.assignPositiveIntegerSetting(next, 'firstDataConnectTimeoutMs', formData.firstDataConnectTimeoutMs, 'First Data Timeout');
|
||||
|
||||
const serverFirstPorts = this.parsePortList(formData.serverFirstPorts, 'Server-first Ports');
|
||||
if (serverFirstPorts.length > 0) {
|
||||
if (serverFirstPorts.includes(443)) {
|
||||
throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
|
||||
}
|
||||
next.serverFirstPorts = serverFirstPorts;
|
||||
}
|
||||
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
|
||||
private assignPositiveIntegerSetting(
|
||||
target: interfaces.data.IRemoteIngressPerformanceConfig,
|
||||
key: 'maxStreamsPerEdge' | 'clientWriteTimeoutMs' | 'firstDataConnectTimeoutMs',
|
||||
value: any,
|
||||
label: string,
|
||||
): void {
|
||||
const text = `${value || ''}`.trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const parsed = Number.parseInt(text, 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 1) {
|
||||
throw new Error(`${label} must be a positive integer`);
|
||||
}
|
||||
target[key] = parsed;
|
||||
}
|
||||
|
||||
private parsePortList(value: any, label: string): number[] {
|
||||
const text = `${value || ''}`.trim();
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
const ports = text.split(',').map((part) => Number.parseInt(part.trim(), 10));
|
||||
for (const port of ports) {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`${label} must contain valid port numbers`);
|
||||
}
|
||||
}
|
||||
return [...new Set(ports)].sort((a, b) => a - b);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user