Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fbe2eb80b | |||
| a22cc1c0eb | |||
| 4ea339b85a | |||
| df9cc3e49b | |||
| 7f3ab2499d | |||
| 89ab918826 | |||
| e5c3578163 | |||
| 1567606c49 | |||
| af31982d58 |
@@ -1,4 +1,4 @@
|
|||||||
name: Docker (tags)
|
name: Docker (non-tag pushes)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -8,42 +8,10 @@ on:
|
|||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
|
||||||
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
|
|
||||||
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ${{ env.IMAGE }}
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install pnpm and npmci
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @shipzone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Audit production dependencies
|
|
||||||
run: |
|
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
npmci command pnpm audit --audit-level=high --prod
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Audit development dependencies
|
|
||||||
run: |
|
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
npmci command pnpm audit --audit-level=high --dev
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: security
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ${{ env.IMAGE }}
|
image: ${{ env.IMAGE }}
|
||||||
@@ -54,18 +22,14 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @git.zone/tsdocker@latest
|
||||||
npmci npm prepare
|
pnpm install
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test
|
||||||
run: |
|
run: pnpm test
|
||||||
npmci node install stable
|
|
||||||
npmci npm install
|
|
||||||
npmci npm test
|
|
||||||
|
|
||||||
- name: Test build
|
- name: Build image
|
||||||
run: |
|
run: tsdocker build
|
||||||
npmci npm prepare
|
|
||||||
npmci node install stable
|
- name: Test image
|
||||||
npmci npm install
|
run: tsdocker test
|
||||||
npmci command npm run build
|
|
||||||
|
|||||||
@@ -8,73 +8,13 @@ on:
|
|||||||
env:
|
env:
|
||||||
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
|
||||||
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
|
|
||||||
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ${{ env.IMAGE }}
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @shipzone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Audit production dependencies
|
|
||||||
run: |
|
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
npmci command pnpm audit --audit-level=high --prod
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Audit development dependencies
|
|
||||||
run: |
|
|
||||||
npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
npmci command pnpm audit --audit-level=high --dev
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
test:
|
|
||||||
needs: security
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ${{ env.IMAGE }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
pnpm install -g pnpm
|
|
||||||
pnpm install -g @shipzone/npmci
|
|
||||||
npmci npm prepare
|
|
||||||
|
|
||||||
- name: Test stable
|
|
||||||
run: |
|
|
||||||
npmci node install stable
|
|
||||||
npmci npm install
|
|
||||||
npmci npm test
|
|
||||||
|
|
||||||
- name: Test build
|
|
||||||
run: |
|
|
||||||
npmci node install stable
|
|
||||||
npmci npm install
|
|
||||||
npmci command npm run build
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: test
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: code.foss.global/host.today/ht-docker-node:dbase_dind
|
image: code.foss.global/host.today/ht-docker-dbase:szci
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -82,23 +22,20 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @git.zone/tsdocker
|
pnpm install -g @git.zone/tsdocker@latest
|
||||||
|
pnpm install
|
||||||
|
|
||||||
- name: Release
|
- name: Login to registries
|
||||||
run: |
|
run: tsdocker login
|
||||||
tsdocker login
|
|
||||||
tsdocker build
|
|
||||||
tsdocker push
|
|
||||||
|
|
||||||
metadata:
|
- name: List images
|
||||||
needs: test
|
run: tsdocker list
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ${{ env.IMAGE }}
|
|
||||||
|
|
||||||
steps:
|
- name: Build images
|
||||||
- uses: actions/checkout@v3
|
run: tsdocker build
|
||||||
|
|
||||||
- name: Trigger
|
- name: Test images
|
||||||
run: npmci trigger
|
run: tsdocker test
|
||||||
|
|
||||||
|
- name: Push to code.foss.global
|
||||||
|
run: tsdocker push code.foss.global
|
||||||
|
|||||||
+2
-10
@@ -30,7 +30,7 @@
|
|||||||
"@git.zone/cli": {
|
"@git.zone/cli": {
|
||||||
"projectType": "service",
|
"projectType": "service",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "serve.zone",
|
"gitscope": "serve.zone",
|
||||||
"gitrepo": "dcrouter",
|
"gitrepo": "dcrouter",
|
||||||
"description": "A traffic router intended to be gating your datacenter.",
|
"description": "A traffic router intended to be gating your datacenter.",
|
||||||
@@ -67,18 +67,10 @@
|
|||||||
"accessLevel": "public"
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {
|
|
||||||
"npmGlobalTools": [],
|
|
||||||
"dockerRegistryRepoMap": {
|
|
||||||
"registry.gitlab.com": "code.foss.global/serve.zone/dcrouter"
|
|
||||||
},
|
|
||||||
"npmRegistryUrl": "verdaccio.lossless.digital"
|
|
||||||
},
|
|
||||||
"@git.zone/tsdocker": {
|
"@git.zone/tsdocker": {
|
||||||
"registries": ["code.foss.global"],
|
"registries": ["code.foss.global"],
|
||||||
"registryRepoMap": {
|
"registryRepoMap": {
|
||||||
"code.foss.global": "serve.zone/dcrouter",
|
"code.foss.global": "serve.zone/dcrouter"
|
||||||
"dockerregistry.lossless.digital": "serve.zone/dcrouter"
|
|
||||||
},
|
},
|
||||||
"platforms": ["linux/amd64", "linux/arm64"]
|
"platforms": ["linux/amd64", "linux/arm64"]
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -1,12 +1,17 @@
|
|||||||
# gitzone dockerfile_service
|
# gitzone dockerfile_service
|
||||||
## STAGE 1 // BUILD
|
## STAGE 1 // BUILD
|
||||||
FROM code.foss.global/host.today/ht-docker-node:lts AS build
|
FROM code.foss.global/host.today/ht-docker-node:lts AS build
|
||||||
COPY ./ /app
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
RUN pnpm config set store-dir .pnpm-store
|
RUN pnpm config set store-dir .pnpm-store
|
||||||
RUN rm -rf node_modules && pnpm install
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
RUN rm -rf .pnpm-store node_modules && pnpm install --prod
|
RUN rm -rf .pnpm-store
|
||||||
|
RUN pnpm prune --prod
|
||||||
|
|
||||||
## STAGE 2 // PRODUCTION
|
## STAGE 2 // PRODUCTION
|
||||||
FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production
|
FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production
|
||||||
@@ -18,12 +23,10 @@ WORKDIR /app
|
|||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
ENV DCROUTER_MODE=OCI_CONTAINER
|
ENV DCROUTER_MODE=OCI_CONTAINER
|
||||||
|
ENV NODE_ENV=production
|
||||||
ENV DCROUTER_HEAP_SIZE=512
|
ENV DCROUTER_HEAP_SIZE=512
|
||||||
ENV UV_THREADPOOL_SIZE=16
|
ENV UV_THREADPOOL_SIZE=16
|
||||||
|
|
||||||
RUN pnpm install -g @servezone/healthy
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 CMD [ "healthy" ]
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="dcrouter" \
|
LABEL org.opencontainers.image.title="dcrouter" \
|
||||||
org.opencontainers.image.description="Multi-service datacenter gateway" \
|
org.opencontainers.image.description="Multi-service datacenter gateway" \
|
||||||
org.opencontainers.image.source="https://code.foss.global/serve.zone/dcrouter"
|
org.opencontainers.image.source="https://code.foss.global/serve.zone/dcrouter"
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.25.0 - feat(security)
|
||||||
|
compile network ranges and CIDR arrays into edge firewall policies
|
||||||
|
|
||||||
|
- add support for storing intelligence network CIDR arrays alongside single network ranges
|
||||||
|
- convert start-end IPv4 ranges into CIDR blocks when compiling security policies
|
||||||
|
- always return an explicit remote ingress firewall snapshot with a blockedIps array
|
||||||
|
- add tests covering range normalization, ASN-derived CIDRs, and empty firewall snapshots
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.24.0 - feat(security)
|
||||||
|
add security policy management and IP intelligence operations to the ops UI
|
||||||
|
|
||||||
|
- adds typed request endpoints to fetch compiled security policy, list audit events, and force-refresh IP intelligence
|
||||||
|
- introduces dedicated security policy state and actions for loading, creating, updating, deleting, and refreshing security data
|
||||||
|
- enhances the network activity view with IP intelligence columns, detail dialogs, and block-rule actions
|
||||||
|
- expands the security blocked view into a full management interface for rules, compiled policy, IP intelligence, and audit history
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.23.0 - feat(security)
|
||||||
|
add managed security policies with IP intelligence and remote ingress firewall propagation
|
||||||
|
|
||||||
|
- introduces a SecurityPolicyManager that observes public IPs, stores IP intelligence, compiles block policies, and audits policy changes
|
||||||
|
- adds database documents and shared interfaces for security block rules, IP intelligence records, and security policy audit events
|
||||||
|
- exposes ops/admin request handlers to list IP intelligence and create, update, or delete security block rules
|
||||||
|
- applies merged security policies to SmartProxy and propagates firewall snapshots to remote ingress edges and tunnel synchronization
|
||||||
|
|
||||||
## 2026-04-26 - 13.22.0 - feat(remoteingress)
|
## 2026-04-26 - 13.22.0 - feat(remoteingress)
|
||||||
add remote ingress performance configuration and expose tunnel transport metrics
|
add remote ingress performance configuration and expose tunnel transport metrics
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "13.22.0",
|
"version": "13.25.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.4.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.10.0",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
|
"@git.zone/tsdocker": "^2.2.4",
|
||||||
"@git.zone/tsrun": "^2.0.2",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.6.3",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@git.zone/tswatch": "^3.3.2",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
@@ -51,10 +52,10 @@
|
|||||||
"@push.rocks/smartmetrics": "^3.0.3",
|
"@push.rocks/smartmetrics": "^3.0.3",
|
||||||
"@push.rocks/smartmigration": "1.2.0",
|
"@push.rocks/smartmigration": "1.2.0",
|
||||||
"@push.rocks/smartmta": "^5.3.3",
|
"@push.rocks/smartmta": "^5.3.3",
|
||||||
"@push.rocks/smartnetwork": "^4.6.0",
|
"@push.rocks/smartnetwork": "^4.7.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^27.8.2",
|
"@push.rocks/smartproxy": "^27.9.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
@@ -63,8 +64,8 @@
|
|||||||
"@push.rocks/smartvpn": "1.19.2",
|
"@push.rocks/smartvpn": "1.19.2",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.12.4",
|
"@serve.zone/catalog": "^2.12.4",
|
||||||
"@serve.zone/interfaces": "^5.4.3",
|
"@serve.zone/interfaces": "^5.4.6",
|
||||||
"@serve.zone/remoteingress": "^4.17.0",
|
"@serve.zone/remoteingress": "^4.17.1",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"lru-cache": "^11.3.5",
|
"lru-cache": "^11.3.5",
|
||||||
|
|||||||
Generated
+242
-24
@@ -72,8 +72,8 @@ importers:
|
|||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
'@push.rocks/smartnetwork':
|
'@push.rocks/smartnetwork':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.7.0
|
||||||
version: 4.6.0
|
version: 4.7.0
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -81,8 +81,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^27.8.2
|
specifier: ^27.9.0
|
||||||
version: 27.8.2
|
version: 27.9.0
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -108,11 +108,11 @@ importers:
|
|||||||
specifier: ^2.12.4
|
specifier: ^2.12.4
|
||||||
version: 2.12.4(@tiptap/pm@2.27.2)
|
version: 2.12.4(@tiptap/pm@2.27.2)
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.4.3
|
specifier: ^5.4.6
|
||||||
version: 5.4.3
|
version: 5.4.6
|
||||||
'@serve.zone/remoteingress':
|
'@serve.zone/remoteingress':
|
||||||
specifier: ^4.17.0
|
specifier: ^4.17.1
|
||||||
version: 4.17.0
|
version: 4.17.1
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.5.0
|
specifier: ^9.5.0
|
||||||
version: 9.5.0
|
version: 9.5.0
|
||||||
@@ -135,6 +135,9 @@ importers:
|
|||||||
'@git.zone/tsbundle':
|
'@git.zone/tsbundle':
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.10.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
|
version: 2.10.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
|
||||||
|
'@git.zone/tsdocker':
|
||||||
|
specifier: ^2.2.4
|
||||||
|
version: 2.2.4
|
||||||
'@git.zone/tsrun':
|
'@git.zone/tsrun':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
@@ -170,6 +173,9 @@ packages:
|
|||||||
'@apiclient.xyz/cloudflare@7.1.0':
|
'@apiclient.xyz/cloudflare@7.1.0':
|
||||||
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
|
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
|
||||||
|
|
||||||
|
'@apiglobal/typedrequest-interfaces@1.0.20':
|
||||||
|
resolution: {integrity: sha512-ybsDtavYbzGJYSLodSbkxDvSLYtfMzBTuNZDJpiANt1rZA2MO/GCq8zk5MVLlrUUQIr/7oxPGWqxi1QDwR+RHQ==}
|
||||||
|
|
||||||
'@aws-crypto/crc32@5.2.0':
|
'@aws-crypto/crc32@5.2.0':
|
||||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -561,6 +567,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-dw2VFlgKssDlCxg92wSPiiAKwfCjJBOEOYXq1xO91OpjQLOkyogCxSLy0jzQ2BYnt4qmBnapjamzYzVjCr4CWg==}
|
resolution: {integrity: sha512-dw2VFlgKssDlCxg92wSPiiAKwfCjJBOEOYXq1xO91OpjQLOkyogCxSLy0jzQ2BYnt4qmBnapjamzYzVjCr4CWg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@git.zone/tsdocker@2.2.4':
|
||||||
|
resolution: {integrity: sha512-B5N8I159R0X9NOrYWx3kLQPuIW71uXKzb+RCS4h9N5FSlCOWVPDUU4yuv0dl24lWHsQmSgcnSqPRAUxhSCqZng==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.5':
|
'@git.zone/tspublish@1.11.5':
|
||||||
resolution: {integrity: sha512-3tCGhVbH6S/17n3A6Tc6H+ncRdxxbTT0ABcj8S1wRLA8YuBSj9bY7k6uj/iFRy/B/OepB94m1goCiaWESdcZYg==}
|
resolution: {integrity: sha512-3tCGhVbH6S/17n3A6Tc6H+ncRdxxbTT0ABcj8S1wRLA8YuBSj9bY7k6uj/iFRy/B/OepB94m1goCiaWESdcZYg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1213,6 +1223,9 @@ packages:
|
|||||||
'@push.rocks/smartlog-interfaces@3.0.2':
|
'@push.rocks/smartlog-interfaces@3.0.2':
|
||||||
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
||||||
|
|
||||||
|
'@push.rocks/smartlog-source-ora@1.0.9':
|
||||||
|
resolution: {integrity: sha512-s5OmwceGUFbCysYNg3VJZo07lkHxD2GPk8VABJTmhxtrogBw5kChx9d5NMdWQ+CovkNoNhKen1hF3b3l0v6jSQ==}
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.2.2':
|
'@push.rocks/smartlog@3.2.2':
|
||||||
resolution: {integrity: sha512-3Nw/Ki/jZ4vrrWnEtpcGPF28jQ+fr9/9Edc7ytaEA6ZWIpojtwacJ5qihMvHbIei+zjpD35w6tZP2mQjvw5VRQ==}
|
resolution: {integrity: sha512-3Nw/Ki/jZ4vrrWnEtpcGPF28jQ+fr9/9Edc7ytaEA6ZWIpojtwacJ5qihMvHbIei+zjpD35w6tZP2mQjvw5VRQ==}
|
||||||
|
|
||||||
@@ -1257,12 +1270,15 @@ packages:
|
|||||||
'@push.rocks/smartmustache@3.0.2':
|
'@push.rocks/smartmustache@3.0.2':
|
||||||
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
|
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
|
||||||
|
|
||||||
'@push.rocks/smartnetwork@4.6.0':
|
'@push.rocks/smartnetwork@4.7.0':
|
||||||
resolution: {integrity: sha512-ubaO/Qp8r30A+qwk33M/0+nQi+o8gNHEI9zq3jv1MwqiLxhiV1hnbr4CL9AUcvs4EhwUBiw0EswKjCJROwDqvQ==}
|
resolution: {integrity: sha512-WZ46pJlklDRcw1AqkyyBhmGSNSK3i7IYM9D9vcVJOUhlLmgUSai8o1NbpWlb7HvOkp1IhQ7iZeuJV2JiWLtl1g==}
|
||||||
|
|
||||||
'@push.rocks/smartnftables@1.1.0':
|
'@push.rocks/smartnftables@1.1.0':
|
||||||
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
|
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.2.0':
|
||||||
|
resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==}
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
||||||
|
|
||||||
@@ -1284,8 +1300,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.8.2':
|
'@push.rocks/smartproxy@27.9.0':
|
||||||
resolution: {integrity: sha512-4T20SKk4oewAg/ztazxxtkHIip3lM0ksZmXZN/zx2uC68HdZRroK5oekMYcIeD2AfvjGYUK1vI1MMgQz+glHXQ==}
|
resolution: {integrity: sha512-lzOxueA89pBf4ZcTzF+VkjXQ0es8z8C20PW6FA0HcIzcCpnh4NjLwnXyD8NnTpCf+HKh/EAgD77Kt9Dn+sssUQ==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1401,6 +1417,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9OJbnRgLTaCRQz+pqu5tB3ZCqRs5Zh0hnBe7t7URE+TgwIZ8aiELUIbWRkgn4mSGVzHyL6pqTyIowP6AjUCG3w==}
|
resolution: {integrity: sha512-9OJbnRgLTaCRQz+pqu5tB3ZCqRs5Zh0hnBe7t7URE+TgwIZ8aiELUIbWRkgn4mSGVzHyL6pqTyIowP6AjUCG3w==}
|
||||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartjson
|
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartjson
|
||||||
|
|
||||||
|
'@pushrocks/smartlog-interfaces@2.0.23':
|
||||||
|
resolution: {integrity: sha512-tXqwfrekGxGZJB72BFQppywk7413hXVDgcJNeU+kY6xvmzVjf2HxOMbFYhewh1+p4uai1u9n0xcMb0qbbPy4/Q==}
|
||||||
|
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartlog-interfaces
|
||||||
|
|
||||||
'@pushrocks/smartpromise@3.1.10':
|
'@pushrocks/smartpromise@3.1.10':
|
||||||
resolution: {integrity: sha512-VeTurbZ1+ZMxBDJk1Y1LV8SN9xLI+oDXKVeCFw41FAGEKOUEqordqFpi6t+7Vhe/TXUZzCVpZ5bXxAxrGf8yTQ==}
|
resolution: {integrity: sha512-VeTurbZ1+ZMxBDJk1Y1LV8SN9xLI+oDXKVeCFw41FAGEKOUEqordqFpi6t+7Vhe/TXUZzCVpZ5bXxAxrGf8yTQ==}
|
||||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartpromise
|
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartpromise
|
||||||
@@ -1591,11 +1611,11 @@ packages:
|
|||||||
'@serve.zone/catalog@2.12.4':
|
'@serve.zone/catalog@2.12.4':
|
||||||
resolution: {integrity: sha512-GRfJZ0yQxChUy7Gp4mxhuN5y4GXZMOEk0W7rJiyZbezA938q+pFTplb9ahSaEHjiUht1MmTu/5WtoJFwgAP8SQ==}
|
resolution: {integrity: sha512-GRfJZ0yQxChUy7Gp4mxhuN5y4GXZMOEk0W7rJiyZbezA938q+pFTplb9ahSaEHjiUht1MmTu/5WtoJFwgAP8SQ==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.4.3':
|
'@serve.zone/interfaces@5.4.6':
|
||||||
resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==}
|
resolution: {integrity: sha512-o4k7Wr6t3NLiP6gfAZZz8Jx8RlQ4sZYHTbhr4WkXzGf78vczFRIuFLyY1Y+TTNzDLEIzLVIyMsuECMV1KTwB2Q==}
|
||||||
|
|
||||||
'@serve.zone/remoteingress@4.17.0':
|
'@serve.zone/remoteingress@4.17.1':
|
||||||
resolution: {integrity: sha512-q1g2Zm1Yh825cMiF8/W1iQlOLGqgmWBrtzDqNgF5hH31HP2zHHtC2+XPyB+1kEphsztlXzPMlcRpfCRwuQUexA==}
|
resolution: {integrity: sha512-k3n+AF1rNybiKPlHHyhwCVEF0/T7eZD46kNn7JlEJPCxfUy09mjkpwDQ2CzaUkppqNgFOAYXgAKqjDqpJ27RvA==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
@@ -2151,6 +2171,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@3.2.1:
|
||||||
|
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2328,6 +2352,14 @@ packages:
|
|||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
chalk@2.4.2:
|
||||||
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
chalk@3.0.0:
|
||||||
|
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
character-entities-html4@2.1.0:
|
character-entities-html4@2.1.0:
|
||||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||||
|
|
||||||
@@ -2353,6 +2385,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==}
|
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==}
|
||||||
engines: {node: '>= 4.0'}
|
engines: {node: '>= 4.0'}
|
||||||
|
|
||||||
|
cli-cursor@3.1.0:
|
||||||
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
cli-spinners@2.9.2:
|
||||||
|
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
cli-width@4.1.0:
|
cli-width@4.1.0:
|
||||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
@@ -2364,13 +2404,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
clone@1.0.4:
|
||||||
|
resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
cloudflare@5.2.0:
|
cloudflare@5.2.0:
|
||||||
resolution: {integrity: sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A==}
|
resolution: {integrity: sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A==}
|
||||||
|
|
||||||
|
color-convert@1.9.3:
|
||||||
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.3:
|
||||||
|
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
|
||||||
|
|
||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
@@ -2455,6 +2505,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
defaults@1.0.4:
|
||||||
|
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
||||||
|
|
||||||
defer-to-connect@2.0.1:
|
defer-to-connect@2.0.1:
|
||||||
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2568,6 +2621,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
escape-string-regexp@1.0.5:
|
||||||
|
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
||||||
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0:
|
escape-string-regexp@4.0.0:
|
||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2799,6 +2856,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==}
|
resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
has-flag@3.0.0:
|
||||||
|
resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
|
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
|
||||||
|
|
||||||
@@ -2921,6 +2986,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-interactive@1.0.0:
|
||||||
|
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-nan@1.3.2:
|
is-nan@1.3.2:
|
||||||
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
|
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3075,6 +3144,10 @@ packages:
|
|||||||
lodash.once@4.1.1:
|
lodash.once@4.1.1:
|
||||||
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
|
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
|
||||||
|
|
||||||
|
log-symbols@3.0.0:
|
||||||
|
resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@@ -3286,6 +3359,10 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mimic-fn@2.1.0:
|
||||||
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
mimic-response@3.1.0:
|
mimic-response@3.1.0:
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3397,6 +3474,9 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
mute-stream@0.0.8:
|
||||||
|
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
|
||||||
|
|
||||||
mute-stream@1.0.0:
|
mute-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
|
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
@@ -3470,10 +3550,18 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
||||||
|
|
||||||
|
onetime@5.1.2:
|
||||||
|
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
open@8.4.2:
|
open@8.4.2:
|
||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ora@4.1.1:
|
||||||
|
resolution: {integrity: sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
orderedmap@2.1.1:
|
orderedmap@2.1.1:
|
||||||
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
|
||||||
|
|
||||||
@@ -3824,6 +3912,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
|
resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
restore-cursor@3.1.0:
|
||||||
|
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3979,6 +4071,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==}
|
resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
sweet-scroll@4.0.0:
|
sweet-scroll@4.0.0:
|
||||||
resolution: {integrity: sha512-mR6fRsAQANtm3zpzhUE73KAOt2aT4ZsWzNSggiEsSqdO6Zh4gM7ioJG81EngrZEl0XAc3ZvzEfhxggOoEBc8jA==}
|
resolution: {integrity: sha512-mR6fRsAQANtm3zpzhUE73KAOt2aT4ZsWzNSggiEsSqdO6Zh4gM7ioJG81EngrZEl0XAc3ZvzEfhxggOoEBc8jA==}
|
||||||
|
|
||||||
@@ -4171,6 +4271,9 @@ packages:
|
|||||||
w3c-keyname@2.2.8:
|
w3c-keyname@2.2.8:
|
||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
|
wcwidth@1.0.1:
|
||||||
|
resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=}
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3:
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -4395,6 +4498,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
'@apiglobal/typedrequest-interfaces@1.0.20': {}
|
||||||
|
|
||||||
'@aws-crypto/crc32@5.2.0':
|
'@aws-crypto/crc32@5.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/util': 5.2.0
|
'@aws-crypto/util': 5.2.0
|
||||||
@@ -5115,6 +5220,24 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@git.zone/tsdocker@2.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/lik': 6.4.0
|
||||||
|
'@push.rocks/projectinfo': 5.1.0
|
||||||
|
'@push.rocks/smartcli': 4.0.20
|
||||||
|
'@push.rocks/smartconfig': 6.1.0
|
||||||
|
'@push.rocks/smartfs': 1.5.0
|
||||||
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
|
'@push.rocks/smartlog-source-ora': 1.0.9
|
||||||
|
'@push.rocks/smartshell': 3.3.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.5':
|
'@git.zone/tspublish@1.11.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -5160,7 +5283,7 @@ snapshots:
|
|||||||
'@push.rocks/smartjson': 6.0.0
|
'@push.rocks/smartjson': 6.0.0
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
'@push.rocks/smartmongo': 5.1.1(socks@2.8.7)
|
'@push.rocks/smartmongo': 5.1.1(socks@2.8.7)
|
||||||
'@push.rocks/smartnetwork': 4.6.0
|
'@push.rocks/smartnetwork': 4.7.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
@@ -5963,7 +6086,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartdns': 7.9.0
|
'@push.rocks/smartdns': 7.9.0
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
'@push.rocks/smartnetwork': 4.6.0
|
'@push.rocks/smartnetwork': 4.7.0
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
@@ -6322,6 +6445,11 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 2.0.2
|
'@api.global/typedrequest-interfaces': 2.0.2
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
|
'@push.rocks/smartlog-source-ora@1.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@pushrocks/smartlog-interfaces': 2.0.23
|
||||||
|
ora: 4.1.1
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.2.2':
|
'@push.rocks/smartlog@3.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
@@ -6430,7 +6558,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
handlebars: 4.7.9
|
handlebars: 4.7.9
|
||||||
|
|
||||||
'@push.rocks/smartnetwork@4.6.0':
|
'@push.rocks/smartnetwork@4.7.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdns': 7.9.0
|
'@push.rocks/smartdns': 7.9.0
|
||||||
'@push.rocks/smartrust': 1.3.2
|
'@push.rocks/smartrust': 1.3.2
|
||||||
@@ -6443,6 +6571,11 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -6491,7 +6624,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.5.0
|
'@push.rocks/smartfs': 1.5.0
|
||||||
'@push.rocks/smartjimp': 1.2.0
|
'@push.rocks/smartjimp': 1.2.0
|
||||||
'@push.rocks/smartnetwork': 4.6.0
|
'@push.rocks/smartnetwork': 4.7.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2)
|
'@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2)
|
||||||
@@ -6512,7 +6645,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.8.2':
|
'@push.rocks/smartproxy@27.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
@@ -6783,6 +6916,10 @@ snapshots:
|
|||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
|
||||||
|
'@pushrocks/smartlog-interfaces@2.0.23':
|
||||||
|
dependencies:
|
||||||
|
'@apiglobal/typedrequest-interfaces': 1.0.20
|
||||||
|
|
||||||
'@pushrocks/smartpromise@3.1.10': {}
|
'@pushrocks/smartpromise@3.1.10': {}
|
||||||
|
|
||||||
'@pushrocks/smartpromise@4.0.2': {}
|
'@pushrocks/smartpromise@4.0.2': {}
|
||||||
@@ -6927,16 +7064,16 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.4.3':
|
'@serve.zone/interfaces@5.4.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@tsclass/tsclass': 9.5.0
|
'@tsclass/tsclass': 9.5.0
|
||||||
|
|
||||||
'@serve.zone/remoteingress@4.17.0':
|
'@serve.zone/remoteingress@4.17.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartnftables': 1.1.0
|
'@push.rocks/smartnftables': 1.2.0
|
||||||
'@push.rocks/smartrust': 1.3.2
|
'@push.rocks/smartrust': 1.3.2
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@sindresorhus/is@5.6.0': {}
|
||||||
@@ -7634,6 +7771,10 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 1.9.3
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
@@ -7797,6 +7938,17 @@ snapshots:
|
|||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
|
chalk@2.4.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 3.2.1
|
||||||
|
escape-string-regexp: 1.0.5
|
||||||
|
supports-color: 5.5.0
|
||||||
|
|
||||||
|
chalk@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
character-entities-html4@2.1.0: {}
|
character-entities-html4@2.1.0: {}
|
||||||
|
|
||||||
character-entities-legacy@3.0.0: {}
|
character-entities-legacy@3.0.0: {}
|
||||||
@@ -7819,6 +7971,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
|
cli-cursor@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
restore-cursor: 3.1.0
|
||||||
|
|
||||||
|
cli-spinners@2.9.2: {}
|
||||||
|
|
||||||
cli-width@4.1.0: {}
|
cli-width@4.1.0: {}
|
||||||
|
|
||||||
cliui@6.0.0:
|
cliui@6.0.0:
|
||||||
@@ -7833,6 +7991,8 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
|
clone@1.0.4: {}
|
||||||
|
|
||||||
cloudflare@5.2.0:
|
cloudflare@5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.130
|
'@types/node': 18.19.130
|
||||||
@@ -7845,10 +8005,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
color-convert@1.9.3:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.3
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.3: {}
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
@@ -7915,6 +8081,10 @@ snapshots:
|
|||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
|
defaults@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
clone: 1.0.4
|
||||||
|
|
||||||
defer-to-connect@2.0.1: {}
|
defer-to-connect@2.0.1: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
@@ -8050,6 +8220,8 @@ snapshots:
|
|||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
escape-string-regexp@1.0.5: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
@@ -8318,6 +8490,10 @@ snapshots:
|
|||||||
webidl-conversions: 7.0.0
|
webidl-conversions: 7.0.0
|
||||||
whatwg-mimetype: 3.0.0
|
whatwg-mimetype: 3.0.0
|
||||||
|
|
||||||
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
@@ -8467,6 +8643,8 @@ snapshots:
|
|||||||
|
|
||||||
is-fullwidth-code-point@3.0.0: {}
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
is-interactive@1.0.0: {}
|
||||||
|
|
||||||
is-nan@1.3.2:
|
is-nan@1.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -8651,6 +8829,10 @@ snapshots:
|
|||||||
|
|
||||||
lodash.once@4.1.1: {}
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
|
log-symbols@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
chalk: 2.4.2
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
lower-case@1.1.4: {}
|
lower-case@1.1.4: {}
|
||||||
@@ -9050,6 +9232,8 @@ snapshots:
|
|||||||
|
|
||||||
mime@4.1.0: {}
|
mime@4.1.0: {}
|
||||||
|
|
||||||
|
mimic-fn@2.1.0: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
mimic-response@4.0.0: {}
|
mimic-response@4.0.0: {}
|
||||||
@@ -9153,6 +9337,8 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
mute-stream@0.0.8: {}
|
||||||
|
|
||||||
mute-stream@1.0.0: {}
|
mute-stream@1.0.0: {}
|
||||||
|
|
||||||
nanoid@4.0.2: {}
|
nanoid@4.0.2: {}
|
||||||
@@ -9201,12 +9387,27 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
onetime@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
mimic-fn: 2.1.0
|
||||||
|
|
||||||
open@8.4.2:
|
open@8.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-lazy-prop: 2.0.0
|
define-lazy-prop: 2.0.0
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
|
ora@4.1.1:
|
||||||
|
dependencies:
|
||||||
|
chalk: 3.0.0
|
||||||
|
cli-cursor: 3.1.0
|
||||||
|
cli-spinners: 2.9.2
|
||||||
|
is-interactive: 1.0.0
|
||||||
|
log-symbols: 3.0.0
|
||||||
|
mute-stream: 0.0.8
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wcwidth: 1.0.1
|
||||||
|
|
||||||
orderedmap@2.1.1: {}
|
orderedmap@2.1.1: {}
|
||||||
|
|
||||||
os-tmpdir@1.0.2: {}
|
os-tmpdir@1.0.2: {}
|
||||||
@@ -9631,6 +9832,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lowercase-keys: 3.0.0
|
lowercase-keys: 3.0.0
|
||||||
|
|
||||||
|
restore-cursor@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
onetime: 5.1.2
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
@@ -9842,6 +10048,14 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
peek-readable: 5.4.2
|
peek-readable: 5.4.2
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 3.0.0
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
sweet-scroll@4.0.0: {}
|
sweet-scroll@4.0.0: {}
|
||||||
|
|
||||||
symbol-tree@3.2.4: {}
|
symbol-tree@3.2.4: {}
|
||||||
@@ -10046,6 +10260,10 @@ snapshots:
|
|||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
|
wcwidth@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
defaults: 1.0.4
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3: {}
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
webdriver-bidi-protocol@0.4.1: {}
|
webdriver-bidi-protocol@0.4.1: {}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { CertificateHandler } from '../ts/opsserver/handlers/certificate.handler.js';
|
||||||
|
import { AcmeCertDoc, DcRouterDb } from '../ts/db/index.js';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
type TScope = interfaces.data.TApiTokenScope;
|
||||||
|
|
||||||
|
const createTestDb = async () => {
|
||||||
|
const storagePath = plugins.path.join(
|
||||||
|
plugins.os.tmpdir(),
|
||||||
|
`dcrouter-cert-api-token-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
DcRouterDb.resetInstance();
|
||||||
|
const db = DcRouterDb.getInstance({
|
||||||
|
storagePath,
|
||||||
|
dbName: `dcrouter-test-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
});
|
||||||
|
await db.start();
|
||||||
|
await db.getDb().mongoDb.createCollection('__test_init');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async cleanup() {
|
||||||
|
await db.stop();
|
||||||
|
DcRouterDb.resetInstance();
|
||||||
|
await plugins.fs.promises.rm(storagePath, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeApiTokenManager = (scopes: TScope[]) => {
|
||||||
|
const token = {
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'certificate-test-token',
|
||||||
|
scopes,
|
||||||
|
createdBy: 'token-user',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
enabled: true,
|
||||||
|
} as interfaces.data.IStoredApiToken;
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateToken: async (rawToken: string) => rawToken === 'valid-token' ? token : null,
|
||||||
|
hasScope: (storedToken: interfaces.data.IStoredApiToken, scope: TScope) => storedToken.scopes.includes(scope),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupHandler = (scopes: TScope[]) => {
|
||||||
|
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
const opsServerRef: any = {
|
||||||
|
typedrouter,
|
||||||
|
adminHandler: {
|
||||||
|
adminIdentityGuard: {
|
||||||
|
exec: async () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dcRouterRef: {
|
||||||
|
apiTokenManager: makeApiTokenManager(scopes),
|
||||||
|
certificateStatusMap: new Map(),
|
||||||
|
smartProxy: {
|
||||||
|
routeManager: { getRoutes: () => [] },
|
||||||
|
},
|
||||||
|
certProvisionScheduler: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new CertificateHandler(opsServerRef);
|
||||||
|
return { typedrouter, opsServerRef };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fireTypedRequest = async (
|
||||||
|
router: plugins.typedrequest.TypedRouter,
|
||||||
|
method: string,
|
||||||
|
request: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
return await router.routeAndAddResponse({
|
||||||
|
method,
|
||||||
|
request,
|
||||||
|
response: {},
|
||||||
|
correlation: {
|
||||||
|
id: `${method}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
phase: 'request',
|
||||||
|
},
|
||||||
|
} as any, { localRequest: true, skipHooks: true }) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testDbPromise = createTestDb();
|
||||||
|
|
||||||
|
tap.test('CertificateHandler allows API-token export with certificates:read', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
|
||||||
|
const certDoc = new AcmeCertDoc();
|
||||||
|
certDoc.id = 'cert-1';
|
||||||
|
certDoc.domainName = 'example.com';
|
||||||
|
certDoc.created = 1;
|
||||||
|
certDoc.validUntil = 2;
|
||||||
|
certDoc.privateKey = '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----';
|
||||||
|
certDoc.publicKey = '-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----';
|
||||||
|
certDoc.csr = '';
|
||||||
|
await certDoc.save();
|
||||||
|
|
||||||
|
const { typedrouter } = setupHandler(['certificates:read']);
|
||||||
|
const result = await fireTypedRequest(typedrouter, 'exportCertificate', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
domain: 'example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
expect(result.response.success).toEqual(true);
|
||||||
|
expect(result.response.cert.domainName).toEqual('example.com');
|
||||||
|
expect(result.response.cert.privateKey).toContain('BEGIN PRIVATE KEY');
|
||||||
|
expect(result.response.cert.publicKey).toContain('BEGIN CERTIFICATE');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('CertificateHandler rejects API-token export without certificates:read', async () => {
|
||||||
|
const { typedrouter } = setupHandler(['certificates:write']);
|
||||||
|
const result = await fireTypedRequest(typedrouter, 'exportCertificate', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
domain: 'example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error?.text).toEqual('insufficient scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('CertificateHandler allows API-token import with certificates:write', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
|
||||||
|
const { typedrouter, opsServerRef } = setupHandler(['certificates:write']);
|
||||||
|
const result = await fireTypedRequest(typedrouter, 'importCertificate', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
cert: {
|
||||||
|
id: 'cert-2',
|
||||||
|
domainName: 'imported.example.com',
|
||||||
|
created: 3,
|
||||||
|
validUntil: 4,
|
||||||
|
privateKey: '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----',
|
||||||
|
publicKey: '-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----',
|
||||||
|
csr: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
expect(result.response.success).toEqual(true);
|
||||||
|
expect((await AcmeCertDoc.findByDomain('imported.example.com'))?.id).toEqual('cert-2');
|
||||||
|
expect(opsServerRef.dcRouterRef.certificateStatusMap.get('imported.example.com')?.status).toEqual('valid');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('cleanup test db', async () => {
|
||||||
|
const testDb = await testDbPromise;
|
||||||
|
await testDb.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
import { DcRouterDb, IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../ts/db/index.js';
|
||||||
|
import { SecurityPolicyManager } from '../ts/security/index.js';
|
||||||
|
|
||||||
|
const createTestDb = async () => {
|
||||||
|
const storagePath = plugins.path.join(
|
||||||
|
plugins.os.tmpdir(),
|
||||||
|
`dcrouter-security-policy-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
DcRouterDb.resetInstance();
|
||||||
|
const db = DcRouterDb.getInstance({
|
||||||
|
storagePath,
|
||||||
|
dbName: `dcrouter-security-policy-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
});
|
||||||
|
await db.start();
|
||||||
|
await db.getDb().mongoDb.createCollection('__test_init');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async cleanup() {
|
||||||
|
await db.stop();
|
||||||
|
DcRouterDb.resetInstance();
|
||||||
|
await plugins.fs.promises.rm(storagePath, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const testDbPromise = createTestDb();
|
||||||
|
|
||||||
|
const clearTestState = async () => {
|
||||||
|
for (const rule of await SecurityBlockRuleDoc.findAll()) {
|
||||||
|
await rule.delete();
|
||||||
|
}
|
||||||
|
for (const record of await IpIntelligenceDoc.findAll()) {
|
||||||
|
await record.delete();
|
||||||
|
}
|
||||||
|
for (const event of await SecurityPolicyAuditDoc.findRecent(1000)) {
|
||||||
|
await event.delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('SecurityPolicyManager compiles start-end CIDR rules for edge firewall snapshots', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
await clearTestState();
|
||||||
|
const manager = new SecurityPolicyManager();
|
||||||
|
|
||||||
|
await manager.createBlockRule({
|
||||||
|
type: 'cidr',
|
||||||
|
value: '203.0.113.0 - 203.0.113.255',
|
||||||
|
reason: 'test range',
|
||||||
|
});
|
||||||
|
|
||||||
|
const policy = await manager.compilePolicy();
|
||||||
|
expect(policy.blockedCidrs).toEqual(['203.0.113.0/24']);
|
||||||
|
|
||||||
|
const firewall = await manager.compileRemoteIngressFirewall();
|
||||||
|
expect(firewall.blockedIps).toEqual(['203.0.113.0/24']);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SecurityPolicyManager compiles intelligence network ranges for ASN rules', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
await clearTestState();
|
||||||
|
const manager = new SecurityPolicyManager();
|
||||||
|
|
||||||
|
const intelligenceDoc = new IpIntelligenceDoc();
|
||||||
|
intelligenceDoc.ipAddress = '198.51.100.23';
|
||||||
|
intelligenceDoc.asn = 64500;
|
||||||
|
intelligenceDoc.asnOrg = 'Example Network';
|
||||||
|
intelligenceDoc.networkRange = '198.51.100.0 - 198.51.100.127';
|
||||||
|
intelligenceDoc.firstSeenAt = Date.now();
|
||||||
|
intelligenceDoc.lastSeenAt = Date.now();
|
||||||
|
intelligenceDoc.updatedAt = Date.now();
|
||||||
|
intelligenceDoc.seenCount = 1;
|
||||||
|
await intelligenceDoc.save();
|
||||||
|
|
||||||
|
await manager.createBlockRule({
|
||||||
|
type: 'asn',
|
||||||
|
value: 'AS64500',
|
||||||
|
reason: 'test asn range',
|
||||||
|
});
|
||||||
|
|
||||||
|
const policy = await manager.compilePolicy();
|
||||||
|
expect(policy.blockedCidrs).toEqual(['198.51.100.0/25']);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SecurityPolicyManager compiles intelligence CIDR arrays for ASN rules', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
await clearTestState();
|
||||||
|
const manager = new SecurityPolicyManager();
|
||||||
|
|
||||||
|
const intelligenceDoc = new IpIntelligenceDoc();
|
||||||
|
intelligenceDoc.ipAddress = '198.51.100.130';
|
||||||
|
intelligenceDoc.asn = 64501;
|
||||||
|
intelligenceDoc.asnOrg = 'Example Split Network';
|
||||||
|
intelligenceDoc.networkRange = null;
|
||||||
|
intelligenceDoc.networkCidrs = ['198.51.100.128/25', '198.51.101.0/24'];
|
||||||
|
intelligenceDoc.firstSeenAt = Date.now();
|
||||||
|
intelligenceDoc.lastSeenAt = Date.now();
|
||||||
|
intelligenceDoc.updatedAt = Date.now();
|
||||||
|
intelligenceDoc.seenCount = 1;
|
||||||
|
await intelligenceDoc.save();
|
||||||
|
|
||||||
|
await manager.createBlockRule({
|
||||||
|
type: 'asn',
|
||||||
|
value: 'AS64501',
|
||||||
|
reason: 'test asn cidr array',
|
||||||
|
});
|
||||||
|
|
||||||
|
const policy = await manager.compilePolicy();
|
||||||
|
expect(policy.blockedCidrs).toEqual(['198.51.100.128/25', '198.51.101.0/24']);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SecurityPolicyManager returns an explicit empty edge firewall snapshot', async () => {
|
||||||
|
await testDbPromise;
|
||||||
|
await clearTestState();
|
||||||
|
const manager = new SecurityPolicyManager();
|
||||||
|
|
||||||
|
const firewall = await manager.compileRemoteIngressFirewall();
|
||||||
|
expect(firewall).toEqual({ blockedIps: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('cleanup security policy test db', async () => {
|
||||||
|
const dbHandle = await testDbPromise;
|
||||||
|
await clearTestState();
|
||||||
|
await dbHandle.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { WorkAppMailManager } from '../ts/email/classes.workapp-mail-manager.js';
|
||||||
|
import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
|
||||||
|
|
||||||
|
class MemoryStorageManager {
|
||||||
|
public store = new Map<string, string>();
|
||||||
|
|
||||||
|
public async get(key: string): Promise<string | null> {
|
||||||
|
return this.store.get(key) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(key: string, value: string): Promise<void> {
|
||||||
|
this.store.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDcRouterStub = () => {
|
||||||
|
const storageManager = new MemoryStorageManager();
|
||||||
|
const emailConfig: IUnifiedEmailServerOptions = {
|
||||||
|
hostname: 'mail.example.com',
|
||||||
|
ports: [25, 587, 465],
|
||||||
|
domains: [
|
||||||
|
{
|
||||||
|
domain: 'example.com',
|
||||||
|
dnsMode: 'external-dns',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'operator-route',
|
||||||
|
match: { recipients: 'ops@example.com' },
|
||||||
|
action: { type: 'reject', reject: { code: 550, message: 'not here' } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
auth: {
|
||||||
|
users: [{ username: 'operator', password: 'secret' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dcRouterRef: any = {
|
||||||
|
storageManager,
|
||||||
|
options: { emailConfig },
|
||||||
|
emailServer: {
|
||||||
|
updateOptions: (patch: Partial<IUnifiedEmailServerOptions>) => {
|
||||||
|
dcRouterRef.options.emailConfig = {
|
||||||
|
...dcRouterRef.options.emailConfig,
|
||||||
|
...patch,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateEmailRoutes: async (routes: IUnifiedEmailServerOptions['routes']) => {
|
||||||
|
dcRouterRef.options.emailConfig.routes = routes;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { dcRouterRef, storageManager };
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('WorkAppMailManager syncs SMTP identity and inbound smartmta route', async () => {
|
||||||
|
const { dcRouterRef } = createDcRouterStub();
|
||||||
|
const manager = new WorkAppMailManager(dcRouterRef);
|
||||||
|
|
||||||
|
const createResult = await manager.syncMailIdentity({
|
||||||
|
ownership: {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
},
|
||||||
|
localPart: 'Hello',
|
||||||
|
domain: 'Example.com',
|
||||||
|
inbound: {
|
||||||
|
enabled: true,
|
||||||
|
targetHost: '10.0.0.2',
|
||||||
|
targetPort: 2525,
|
||||||
|
},
|
||||||
|
}, 'tester');
|
||||||
|
|
||||||
|
expect(createResult.success).toEqual(true);
|
||||||
|
expect(createResult.action).toEqual('created');
|
||||||
|
expect(createResult.identity?.address).toEqual('hello@example.com');
|
||||||
|
expect(createResult.identity?.smtp.username.startsWith('workapp-')).toEqual(true);
|
||||||
|
expect((createResult.identity as any).smtpPassword).toBeUndefined();
|
||||||
|
expect(createResult.smtpCredentials?.password.length).toBeGreaterThan(20);
|
||||||
|
|
||||||
|
const generatedRoute = dcRouterRef.options.emailConfig.routes.find((route: any) => route.name.startsWith('workapp-mail-'));
|
||||||
|
expect(generatedRoute.match.recipients).toEqual('hello@example.com');
|
||||||
|
expect(generatedRoute.action.forward.host).toEqual('10.0.0.2');
|
||||||
|
expect(generatedRoute.action.forward.port).toEqual(2525);
|
||||||
|
expect(generatedRoute.action.forward.addHeaders['X-Dcrouter-WorkApp-Id']).toEqual('app-1');
|
||||||
|
expect(dcRouterRef.options.emailConfig.routes.some((route: any) => route.name === 'operator-route')).toEqual(true);
|
||||||
|
|
||||||
|
const generatedUser = dcRouterRef.options.emailConfig.auth.users.find((user: any) => user.username.startsWith('workapp-'));
|
||||||
|
expect(generatedUser.password).toEqual(createResult.smtpCredentials?.password);
|
||||||
|
expect(dcRouterRef.options.emailConfig.auth.users.some((user: any) => user.username === 'operator')).toEqual(true);
|
||||||
|
|
||||||
|
const listResult = await manager.listMailIdentities({ workAppId: 'app-1' });
|
||||||
|
expect(listResult.length).toEqual(1);
|
||||||
|
expect(listResult[0].address).toEqual('hello@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkAppMailManager updates, resets credentials, and deletes identities', async () => {
|
||||||
|
const { dcRouterRef } = createDcRouterStub();
|
||||||
|
const manager = new WorkAppMailManager(dcRouterRef);
|
||||||
|
const ownership = {
|
||||||
|
workHosterType: 'onebox' as const,
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResult = await manager.syncMailIdentity({
|
||||||
|
ownership,
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
inbound: { enabled: true, targetHost: '10.0.0.2', targetPort: 2525 },
|
||||||
|
}, 'tester');
|
||||||
|
const firstPassword = createResult.smtpCredentials!.password;
|
||||||
|
|
||||||
|
const updateResult = await manager.syncMailIdentity({
|
||||||
|
ownership,
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
inbound: { enabled: true, targetHost: '10.0.0.3', targetPort: 2526 },
|
||||||
|
}, 'tester');
|
||||||
|
expect(updateResult.action).toEqual('updated');
|
||||||
|
expect(updateResult.smtpCredentials).toBeUndefined();
|
||||||
|
const generatedUser = dcRouterRef.options.emailConfig.auth.users.find((user: any) => user.username.startsWith('workapp-'));
|
||||||
|
expect(generatedUser.password).toEqual(firstPassword);
|
||||||
|
const generatedRoute = dcRouterRef.options.emailConfig.routes.find((route: any) => route.name.startsWith('workapp-mail-'));
|
||||||
|
expect(generatedRoute.action.forward.host).toEqual('10.0.0.3');
|
||||||
|
|
||||||
|
const resetResult = await manager.syncMailIdentity({
|
||||||
|
ownership,
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
resetSmtpPassword: true,
|
||||||
|
}, 'tester');
|
||||||
|
expect(resetResult.smtpCredentials?.password !== firstPassword).toEqual(true);
|
||||||
|
|
||||||
|
const deleteResult = await manager.syncMailIdentity({
|
||||||
|
ownership,
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
delete: true,
|
||||||
|
}, 'tester');
|
||||||
|
expect(deleteResult.action).toEqual('deleted');
|
||||||
|
expect(dcRouterRef.options.emailConfig.routes.some((route: any) => route.name.startsWith('workapp-mail-'))).toEqual(false);
|
||||||
|
expect(dcRouterRef.options.emailConfig.auth.users.some((user: any) => user.username.startsWith('workapp-'))).toEqual(false);
|
||||||
|
expect(dcRouterRef.options.emailConfig.auth.users.some((user: any) => user.username === 'operator')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkAppMailManager applies persisted identities to startup email config', async () => {
|
||||||
|
const { dcRouterRef } = createDcRouterStub();
|
||||||
|
const manager = new WorkAppMailManager(dcRouterRef);
|
||||||
|
await manager.syncMailIdentity({
|
||||||
|
ownership: {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
},
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
inbound: { enabled: true, targetHost: '10.0.0.2', targetPort: 2525 },
|
||||||
|
}, 'tester');
|
||||||
|
|
||||||
|
const baseStartupConfig: IUnifiedEmailServerOptions = {
|
||||||
|
hostname: 'mail.example.com',
|
||||||
|
ports: [25],
|
||||||
|
domains: [{ domain: 'example.com', dnsMode: 'external-dns' }],
|
||||||
|
routes: [],
|
||||||
|
};
|
||||||
|
const startupConfig = await manager.applyStoredIdentitiesToEmailConfig(baseStartupConfig);
|
||||||
|
|
||||||
|
expect(startupConfig.routes.some((route) => route.name.startsWith('workapp-mail-'))).toEqual(true);
|
||||||
|
expect(startupConfig.auth?.users?.some((user) => user.username.startsWith('workapp-'))).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { WorkHosterHandler } from '../ts/opsserver/handlers/workhoster.handler.js';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
type TScope = interfaces.data.TApiTokenScope;
|
||||||
|
|
||||||
|
const fireTypedRequest = async (
|
||||||
|
router: plugins.typedrequest.TypedRouter,
|
||||||
|
method: string,
|
||||||
|
request: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
return await router.routeAndAddResponse({
|
||||||
|
method,
|
||||||
|
request,
|
||||||
|
response: {},
|
||||||
|
correlation: {
|
||||||
|
id: `${method}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
phase: 'request',
|
||||||
|
},
|
||||||
|
} as any, { localRequest: true, skipHooks: true }) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeApiTokenManager = (scopes: TScope[]) => {
|
||||||
|
const token = {
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'workhoster-test-token',
|
||||||
|
scopes,
|
||||||
|
createdBy: 'token-user',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
enabled: true,
|
||||||
|
} as interfaces.data.IStoredApiToken;
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateToken: async (rawToken: string) => rawToken === 'valid-token' ? token : null,
|
||||||
|
hasScope: (storedToken: interfaces.data.IStoredApiToken, scope: TScope) => storedToken.scopes.includes(scope),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeRouteConfigManager = () => {
|
||||||
|
const routes = new Map<string, interfaces.data.IRoute>();
|
||||||
|
let nextRouteNumber = 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
routes,
|
||||||
|
manager: {
|
||||||
|
findApiRouteByExternalKey: (externalKey: string) => {
|
||||||
|
return Array.from(routes.values()).find((route) =>
|
||||||
|
route.origin === 'api' && route.metadata?.externalKey === externalKey,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
createRoute: async (
|
||||||
|
route: interfaces.data.IDcRouterRouteConfig,
|
||||||
|
createdBy: string,
|
||||||
|
enabled = true,
|
||||||
|
metadata?: interfaces.data.IRouteMetadata,
|
||||||
|
) => {
|
||||||
|
const id = `route-${nextRouteNumber++}`;
|
||||||
|
routes.set(id, {
|
||||||
|
id,
|
||||||
|
route,
|
||||||
|
enabled,
|
||||||
|
createdBy,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
origin: 'api',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
updateRoute: async (
|
||||||
|
id: string,
|
||||||
|
patch: {
|
||||||
|
route?: Partial<interfaces.data.IDcRouterRouteConfig>;
|
||||||
|
enabled?: boolean;
|
||||||
|
metadata?: Partial<interfaces.data.IRouteMetadata>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const storedRoute = routes.get(id);
|
||||||
|
if (!storedRoute) return { success: false, message: 'Route not found' };
|
||||||
|
if (patch.route) {
|
||||||
|
storedRoute.route = { ...storedRoute.route, ...patch.route } as interfaces.data.IDcRouterRouteConfig;
|
||||||
|
}
|
||||||
|
if (patch.enabled !== undefined) {
|
||||||
|
storedRoute.enabled = patch.enabled;
|
||||||
|
}
|
||||||
|
if (patch.metadata) {
|
||||||
|
storedRoute.metadata = { ...storedRoute.metadata, ...patch.metadata };
|
||||||
|
}
|
||||||
|
storedRoute.updatedAt = Date.now();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
deleteRoute: async (id: string) => {
|
||||||
|
const deleted = routes.delete(id);
|
||||||
|
return deleted ? { success: true } : { success: false, message: 'Route not found' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupHandler = (options: {
|
||||||
|
scopes: TScope[];
|
||||||
|
dcRouterRef?: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
const opsServerRef: any = {
|
||||||
|
typedrouter,
|
||||||
|
adminHandler: {
|
||||||
|
adminIdentityGuard: {
|
||||||
|
exec: async () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {},
|
||||||
|
apiTokenManager: makeApiTokenManager(options.scopes),
|
||||||
|
...options.dcRouterRef,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new WorkHosterHandler(opsServerRef);
|
||||||
|
return { typedrouter, opsServerRef };
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('WorkHosterHandler exposes capabilities and managed domains with workhosters:read', async () => {
|
||||||
|
const { typedrouter } = setupHandler({
|
||||||
|
scopes: ['workhosters:read'],
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {
|
||||||
|
remoteIngressConfig: { enabled: true },
|
||||||
|
dnsScopes: ['example.com'],
|
||||||
|
http3: { enabled: false },
|
||||||
|
},
|
||||||
|
routeConfigManager: {},
|
||||||
|
smartProxy: {},
|
||||||
|
emailDomainManager: {},
|
||||||
|
emailServer: {},
|
||||||
|
dnsManager: {
|
||||||
|
listDomains: async () => [
|
||||||
|
{ id: 'domain-1', name: 'example.com', source: 'dcrouter', authoritative: true },
|
||||||
|
{ id: 'domain-2', name: 'provider.example', source: 'provider', providerId: 'cloudflare-1', authoritative: false },
|
||||||
|
],
|
||||||
|
toPublicDomain: (domainDoc: any) => ({
|
||||||
|
...domainDoc,
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
createdBy: 'test',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const capabilitiesResult = await fireTypedRequest(typedrouter, 'getGatewayCapabilities', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
});
|
||||||
|
expect(capabilitiesResult.error).toBeUndefined();
|
||||||
|
expect(capabilitiesResult.response.capabilities.routes.idempotentSync).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.domains.read).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.certificates.export).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.email.inbound).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.remoteIngress.enabled).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.dns.authoritative).toEqual(true);
|
||||||
|
expect(capabilitiesResult.response.capabilities.http3.enabled).toEqual(false);
|
||||||
|
|
||||||
|
const domainsResult = await fireTypedRequest(typedrouter, 'getWorkHosterDomains', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
});
|
||||||
|
expect(domainsResult.error).toBeUndefined();
|
||||||
|
expect(domainsResult.response.domains.length).toEqual(2);
|
||||||
|
expect(domainsResult.response.domains[0].capabilities.canCreateSubdomains).toEqual(true);
|
||||||
|
expect(domainsResult.response.domains[1].capabilities.canManageDnsRecords).toEqual(true);
|
||||||
|
expect(domainsResult.response.domains[1].capabilities.canIssueCertificates).toEqual(true);
|
||||||
|
expect(domainsResult.response.domains[1].capabilities.canHostEmail).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkHosterHandler syncs WorkApp routes idempotently with workhosters:write', async () => {
|
||||||
|
const routeConfig = makeRouteConfigManager();
|
||||||
|
const { typedrouter } = setupHandler({
|
||||||
|
scopes: ['workhosters:write'],
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {},
|
||||||
|
routeConfigManager: routeConfig.manager,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ownership: interfaces.data.IWorkAppRouteOwnership = {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
hostname: 'app.example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership,
|
||||||
|
route: {
|
||||||
|
match: { ports: [443], domains: ['app.example.com'] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: '10.0.0.2', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResult.error).toBeUndefined();
|
||||||
|
expect(createResult.response).toEqual({ success: true, action: 'created', routeId: 'route-1' });
|
||||||
|
expect(routeConfig.routes.size).toEqual(1);
|
||||||
|
|
||||||
|
const createdRoute = routeConfig.routes.get('route-1')!;
|
||||||
|
expect(createdRoute.createdBy).toEqual('token-user');
|
||||||
|
expect(createdRoute.route.name?.startsWith('workapp-onebox-box-1-app-1-app-example-com')).toEqual(true);
|
||||||
|
expect(createdRoute.metadata).toEqual({
|
||||||
|
ownerType: 'workhoster',
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
externalKey: 'onebox:box-1:app-1:app.example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership,
|
||||||
|
enabled: false,
|
||||||
|
route: {
|
||||||
|
name: 'updated-workapp-route',
|
||||||
|
match: { ports: [443], domains: ['app.example.com'] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: '10.0.0.3', port: 3000 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateResult.error).toBeUndefined();
|
||||||
|
expect(updateResult.response).toEqual({ success: true, action: 'updated', routeId: 'route-1' });
|
||||||
|
expect(routeConfig.routes.size).toEqual(1);
|
||||||
|
expect(routeConfig.routes.get('route-1')?.enabled).toEqual(false);
|
||||||
|
expect(routeConfig.routes.get('route-1')?.route.name).toEqual('updated-workapp-route');
|
||||||
|
expect(routeConfig.routes.get('route-1')?.route.action.targets?.[0].host).toEqual('10.0.0.3');
|
||||||
|
|
||||||
|
const deleteResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership,
|
||||||
|
delete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteResult.error).toBeUndefined();
|
||||||
|
expect(deleteResult.response).toEqual({ success: true, action: 'deleted', routeId: 'route-1' });
|
||||||
|
expect(routeConfig.routes.size).toEqual(0);
|
||||||
|
|
||||||
|
const unchangedResult = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership,
|
||||||
|
delete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(unchangedResult.error).toBeUndefined();
|
||||||
|
expect(unchangedResult.response).toEqual({ success: true, action: 'unchanged' });
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkHosterHandler rejects WorkApp route sync without workhosters:write', async () => {
|
||||||
|
const routeConfig = makeRouteConfigManager();
|
||||||
|
const { typedrouter } = setupHandler({
|
||||||
|
scopes: ['workhosters:read'],
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {},
|
||||||
|
routeConfigManager: routeConfig.manager,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fireTypedRequest(typedrouter, 'syncWorkAppRoute', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership: {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
hostname: 'app.example.com',
|
||||||
|
},
|
||||||
|
delete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error?.text).toEqual('insufficient scope');
|
||||||
|
expect(routeConfig.routes.size).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkHosterHandler exposes and syncs WorkApp mail identities', async () => {
|
||||||
|
const syncedRequests: Array<{ data: any; userId: string }> = [];
|
||||||
|
const identity: interfaces.data.IWorkAppMailIdentity = {
|
||||||
|
id: 'mail-1',
|
||||||
|
externalKey: 'onebox:box-1:app-1:hello@example.com',
|
||||||
|
ownership: {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
},
|
||||||
|
address: 'hello@example.com',
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
enabled: true,
|
||||||
|
inbound: {
|
||||||
|
enabled: true,
|
||||||
|
targetHost: '10.0.0.2',
|
||||||
|
targetPort: 2525,
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
enabled: true,
|
||||||
|
username: 'workapp-user',
|
||||||
|
},
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
createdBy: 'token-user',
|
||||||
|
};
|
||||||
|
const { typedrouter } = setupHandler({
|
||||||
|
scopes: ['workhosters:read', 'workhosters:write'],
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {},
|
||||||
|
workAppMailManager: {
|
||||||
|
listMailIdentities: async (filter: any) => filter.workAppId === 'app-1' ? [identity] : [],
|
||||||
|
syncMailIdentity: async (data: any, userId: string) => {
|
||||||
|
syncedRequests.push({ data, userId });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'created',
|
||||||
|
identity,
|
||||||
|
smtpCredentials: {
|
||||||
|
username: 'workapp-user',
|
||||||
|
password: 'generated-password',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const listResult = await fireTypedRequest(typedrouter, 'getWorkAppMailIdentities', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership: { workAppId: 'app-1' },
|
||||||
|
});
|
||||||
|
expect(listResult.error).toBeUndefined();
|
||||||
|
expect(listResult.response.identities).toEqual([identity]);
|
||||||
|
|
||||||
|
const syncResult = await fireTypedRequest(typedrouter, 'syncWorkAppMailIdentity', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership: identity.ownership,
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
inbound: identity.inbound,
|
||||||
|
});
|
||||||
|
expect(syncResult.error).toBeUndefined();
|
||||||
|
expect(syncResult.response.success).toEqual(true);
|
||||||
|
expect(syncResult.response.smtpCredentials.password).toEqual('generated-password');
|
||||||
|
expect(syncedRequests[0].userId).toEqual('token-user');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WorkHosterHandler rejects WorkApp mail sync without workhosters:write', async () => {
|
||||||
|
const { typedrouter } = setupHandler({
|
||||||
|
scopes: ['workhosters:read'],
|
||||||
|
dcRouterRef: {
|
||||||
|
options: {},
|
||||||
|
workAppMailManager: {
|
||||||
|
syncMailIdentity: async () => ({ success: true }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fireTypedRequest(typedrouter, 'syncWorkAppMailIdentity', {
|
||||||
|
apiToken: 'valid-token',
|
||||||
|
ownership: {
|
||||||
|
workHosterType: 'onebox',
|
||||||
|
workHosterId: 'box-1',
|
||||||
|
workAppId: 'app-1',
|
||||||
|
},
|
||||||
|
localPart: 'hello',
|
||||||
|
domain: 'example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error?.text).toEqual('insufficient scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
node --input-type=module <<'NODE'
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const readJson = (path) => JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||||
|
|
||||||
|
const checks = {
|
||||||
|
packageVersion: readJson('/app/package.json').version,
|
||||||
|
interfacesVersion: readJson('/app/node_modules/@serve.zone/interfaces/package.json').version,
|
||||||
|
remoteingressVersion: readJson('/app/node_modules/@serve.zone/remoteingress/package.json').version,
|
||||||
|
hasCli: fs.existsSync('/app/cli.js'),
|
||||||
|
hasWebBundle: fs.existsSync('/app/dist_serve/bundle.js'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await import('/app/dist_ts/index.js');
|
||||||
|
|
||||||
|
if (checks.packageVersion !== '13.25.0') {
|
||||||
|
throw new Error(`Unexpected dcrouter package version ${checks.packageVersion}`);
|
||||||
|
}
|
||||||
|
if (checks.interfacesVersion !== '5.4.6') {
|
||||||
|
throw new Error(`Unexpected interfaces version ${checks.interfacesVersion}`);
|
||||||
|
}
|
||||||
|
if (checks.remoteingressVersion !== '4.17.1') {
|
||||||
|
throw new Error(`Unexpected remoteingress version ${checks.remoteingressVersion}`);
|
||||||
|
}
|
||||||
|
if (!checks.hasCli) {
|
||||||
|
throw new Error('Missing cli.js');
|
||||||
|
}
|
||||||
|
if (!checks.hasWebBundle) {
|
||||||
|
throw new Error('Missing web bundle');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(checks));
|
||||||
|
NODE
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.22.0',
|
version: '13.25.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-4
@@ -27,12 +27,13 @@ import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
|||||||
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
||||||
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
|
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
|
||||||
import type { TIpAllowEntry } from './config/classes.route-config-manager.js';
|
import type { TIpAllowEntry } from './config/classes.route-config-manager.js';
|
||||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager } from './security/index.js';
|
||||||
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||||
import { DnsManager } from './dns/manager.dns.js';
|
import { DnsManager } from './dns/manager.dns.js';
|
||||||
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
||||||
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
|
import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
|
||||||
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
||||||
|
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
@@ -284,6 +285,8 @@ export class DcRouter {
|
|||||||
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
|
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
|
||||||
public acmeConfigManager?: AcmeConfigManager;
|
public acmeConfigManager?: AcmeConfigManager;
|
||||||
public emailDomainManager?: EmailDomainManager;
|
public emailDomainManager?: EmailDomainManager;
|
||||||
|
public workAppMailManager: WorkAppMailManager;
|
||||||
|
public securityPolicyManager?: SecurityPolicyManager;
|
||||||
|
|
||||||
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
||||||
public detectedPublicIp: string | null = null;
|
public detectedPublicIp: string | null = null;
|
||||||
@@ -337,6 +340,7 @@ export class DcRouter {
|
|||||||
this.storageManager = new SmartMtaStorageManager(
|
this.storageManager = new SmartMtaStorageManager(
|
||||||
plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage')
|
plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage')
|
||||||
);
|
);
|
||||||
|
this.workAppMailManager = new WorkAppMailManager(this);
|
||||||
|
|
||||||
// Initialize service manager and register all services
|
// Initialize service manager and register all services
|
||||||
this.serviceManager = new plugins.taskbuffer.ServiceManager({
|
this.serviceManager = new plugins.taskbuffer.ServiceManager({
|
||||||
@@ -471,12 +475,36 @@ export class DcRouter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecurityPolicyManager: optional, depends on DcRouterDb — owns IP intelligence
|
||||||
|
// and compiles the global block policy for SmartProxy and remote ingress edges.
|
||||||
|
if (this.options.dbConfig?.enabled !== false) {
|
||||||
|
this.serviceManager.addService(
|
||||||
|
new plugins.taskbuffer.Service('SecurityPolicyManager')
|
||||||
|
.optional()
|
||||||
|
.dependsOn('DcRouterDb')
|
||||||
|
.withStart(async () => {
|
||||||
|
this.securityPolicyManager = new SecurityPolicyManager({
|
||||||
|
onPolicyChanged: () => this.applySecurityPolicy(),
|
||||||
|
});
|
||||||
|
await this.securityPolicyManager.start();
|
||||||
|
})
|
||||||
|
.withStop(async () => {
|
||||||
|
if (this.securityPolicyManager) {
|
||||||
|
await this.securityPolicyManager.stop();
|
||||||
|
this.securityPolicyManager = undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.withRetry({ maxRetries: 1, baseDelayMs: 500 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
|
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
|
||||||
const smartProxyDeps: string[] = [];
|
const smartProxyDeps: string[] = [];
|
||||||
if (this.options.dbConfig?.enabled !== false) {
|
if (this.options.dbConfig?.enabled !== false) {
|
||||||
smartProxyDeps.push('DcRouterDb');
|
smartProxyDeps.push('DcRouterDb');
|
||||||
smartProxyDeps.push('DnsManager');
|
smartProxyDeps.push('DnsManager');
|
||||||
smartProxyDeps.push('AcmeConfigManager');
|
smartProxyDeps.push('AcmeConfigManager');
|
||||||
|
smartProxyDeps.push('SecurityPolicyManager');
|
||||||
}
|
}
|
||||||
this.serviceManager.addService(
|
this.serviceManager.addService(
|
||||||
new plugins.taskbuffer.Service('SmartProxy')
|
new plugins.taskbuffer.Service('SmartProxy')
|
||||||
@@ -971,6 +999,12 @@ export class DcRouter {
|
|||||||
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
|
||||||
|
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
||||||
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
||||||
|
compiledSecurityPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
// If we have routes or need a basic SmartProxy instance, create it
|
// If we have routes or need a basic SmartProxy instance, create it
|
||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
||||||
@@ -1002,6 +1036,7 @@ export class DcRouter {
|
|||||||
// --- always set by dcrouter (after spread) ---
|
// --- always set by dcrouter (after spread) ---
|
||||||
routes,
|
routes,
|
||||||
acme: acmeConfig,
|
acme: acmeConfig,
|
||||||
|
...(mergedSecurityPolicy ? { securityPolicy: mergedSecurityPolicy } as any : {}),
|
||||||
certStore: {
|
certStore: {
|
||||||
loadAll: async () => {
|
loadAll: async () => {
|
||||||
const docs = await ProxyCertDoc.findAll();
|
const docs = await ProxyCertDoc.findAll();
|
||||||
@@ -1245,6 +1280,58 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async applySecurityPolicy(): Promise<void> {
|
||||||
|
if (!this.securityPolicyManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiledSmartProxyPolicy = await this.securityPolicyManager.compileSmartProxyPolicy();
|
||||||
|
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
||||||
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
||||||
|
compiledSmartProxyPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.smartProxy && mergedSecurityPolicy) {
|
||||||
|
const smartProxyWithPolicyApi = this.smartProxy as any;
|
||||||
|
if (typeof smartProxyWithPolicyApi.updateSecurityPolicy === 'function') {
|
||||||
|
await smartProxyWithPolicyApi.updateSecurityPolicy(mergedSecurityPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
|
||||||
|
if (this.remoteIngressManager) {
|
||||||
|
(this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
|
||||||
|
}
|
||||||
|
if (this.tunnelManager) {
|
||||||
|
await this.tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeSecurityPolicies(
|
||||||
|
...policies: Array<Partial<ISecurityCompiledPolicy> | undefined>
|
||||||
|
): ISecurityCompiledPolicy | undefined {
|
||||||
|
const blockedIps = new Set<string>();
|
||||||
|
const blockedCidrs = new Set<string>();
|
||||||
|
|
||||||
|
for (const policy of policies) {
|
||||||
|
for (const ip of policy?.blockedIps || []) {
|
||||||
|
if (ip) blockedIps.add(ip);
|
||||||
|
}
|
||||||
|
for (const cidr of policy?.blockedCidrs || []) {
|
||||||
|
if (cidr) blockedCidrs.add(cidr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockedIps.size === 0 && blockedCidrs.size === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedIps: [...blockedIps].sort(),
|
||||||
|
blockedCidrs: [...blockedCidrs].sort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1545,7 +1632,7 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create config with mapped ports
|
// Create config with mapped ports
|
||||||
const emailConfig: IUnifiedEmailServerOptions = {
|
const emailConfig: IUnifiedEmailServerOptions = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
|
||||||
...this.options.emailConfig,
|
...this.options.emailConfig,
|
||||||
domains: transformedDomains,
|
domains: transformedDomains,
|
||||||
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
||||||
@@ -1555,7 +1642,7 @@ export class DcRouter {
|
|||||||
persistentPath: plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-queue'),
|
persistentPath: plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-queue'),
|
||||||
...this.options.emailConfig.queue,
|
...this.options.emailConfig.queue,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
// Create unified email server
|
// Create unified email server
|
||||||
this.emailServer = new UnifiedEmailServer(this, emailConfig);
|
this.emailServer = new UnifiedEmailServer(this, emailConfig);
|
||||||
@@ -2232,6 +2319,9 @@ export class DcRouter {
|
|||||||
// Initialize the edge registration manager
|
// Initialize the edge registration manager
|
||||||
this.remoteIngressManager = new RemoteIngressManager();
|
this.remoteIngressManager = new RemoteIngressManager();
|
||||||
await this.remoteIngressManager.initialize();
|
await this.remoteIngressManager.initialize();
|
||||||
|
this.remoteIngressManager.setFirewallConfig(
|
||||||
|
await this.securityPolicyManager?.compileRemoteIngressFirewall(),
|
||||||
|
);
|
||||||
|
|
||||||
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
||||||
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
||||||
|
|||||||
@@ -256,6 +256,15 @@ export class RouteConfigManager {
|
|||||||
return this.updateRoute(id, { enabled });
|
return this.updateRoute(id, { enabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findApiRouteByExternalKey(externalKey: string): IRoute | undefined {
|
||||||
|
for (const route of this.routes.values()) {
|
||||||
|
if (route.origin === 'api' && route.metadata?.externalKey === externalKey) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Private: seed routes from constructor config
|
// Private: seed routes from constructor config
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -443,6 +452,15 @@ export class RouteConfigManager {
|
|||||||
lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
|
lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
|
||||||
? metadata.lastResolvedAt
|
? metadata.lastResolvedAt
|
||||||
: undefined,
|
: undefined,
|
||||||
|
ownerType: metadata.ownerType === 'workhoster' || metadata.ownerType === 'operator' || metadata.ownerType === 'system'
|
||||||
|
? metadata.ownerType
|
||||||
|
: undefined,
|
||||||
|
workHosterType: metadata.workHosterType === 'onebox' || metadata.workHosterType === 'cloudly' || metadata.workHosterType === 'custom'
|
||||||
|
? metadata.workHosterType
|
||||||
|
: undefined,
|
||||||
|
workHosterId: normalizeString(metadata.workHosterId),
|
||||||
|
workAppId: normalizeString(metadata.workAppId),
|
||||||
|
externalKey: normalizeString(metadata.externalKey),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!normalized.sourceProfileRef) {
|
if (!normalized.sourceProfileRef) {
|
||||||
@@ -454,6 +472,12 @@ export class RouteConfigManager {
|
|||||||
if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
|
if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
|
||||||
normalized.lastResolvedAt = undefined;
|
normalized.lastResolvedAt = undefined;
|
||||||
}
|
}
|
||||||
|
if (normalized.ownerType !== 'workhoster') {
|
||||||
|
normalized.workHosterType = undefined;
|
||||||
|
normalized.workHosterId = undefined;
|
||||||
|
normalized.workAppId = undefined;
|
||||||
|
normalized.externalKey = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.values(normalized).every((value) => value === undefined)) {
|
if (Object.values(normalized).every((value) => value === undefined)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { IIpIntelligenceRecord } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntelligenceDoc, IpIntelligenceDoc> implements IIpIntelligenceRecord {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public ipAddress!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public asn: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public asnOrg: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public registrantOrg: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public registrantCountry: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public networkRange: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public networkCidrs: string[] | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public abuseContact: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public country: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public countryCode: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public city: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public latitude: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public longitude: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public accuracyRadius: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public timezone: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public firstSeenAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastSeenAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public updatedAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public seenCount: number = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findByIp(ipAddress: string): Promise<IpIntelligenceDoc | null> {
|
||||||
|
return await IpIntelligenceDoc.getInstance({ ipAddress });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findAll(): Promise<IpIntelligenceDoc[]> {
|
||||||
|
return await IpIntelligenceDoc.getInstances({});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { ISecurityBlockRule, TSecurityBlockRuleMatchMode, TSecurityBlockRuleType } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class SecurityBlockRuleDoc extends plugins.smartdata.SmartDataDbDoc<SecurityBlockRuleDoc, SecurityBlockRuleDoc> implements ISecurityBlockRule {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public type!: TSecurityBlockRuleType;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public value!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public enabled: boolean = true;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public reason?: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public updatedAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdBy: string = 'system';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findById(id: string): Promise<SecurityBlockRuleDoc | null> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstance({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findAll(): Promise<SecurityBlockRuleDoc[]> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstances({});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findEnabled(): Promise<SecurityBlockRuleDoc[]> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstances({ enabled: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { ISecurityPolicyAuditEvent } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class SecurityPolicyAuditDoc extends plugins.smartdata.SmartDataDbDoc<SecurityPolicyAuditDoc, SecurityPolicyAuditDoc> implements ISecurityPolicyAuditEvent {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public action!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public actor!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public details!: Record<string, unknown>;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: number = Date.now();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findRecent(limit = 100): Promise<SecurityPolicyAuditDoc[]> {
|
||||||
|
const docs = await SecurityPolicyAuditDoc.getInstances({});
|
||||||
|
return docs.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
// Cached/TTL document classes
|
// Cached/TTL document classes
|
||||||
export * from './classes.cached.email.js';
|
export * from './classes.cached.email.js';
|
||||||
export * from './classes.cached.ip.reputation.js';
|
export * from './classes.cached.ip.reputation.js';
|
||||||
|
export * from './classes.ip-intelligence.doc.js';
|
||||||
|
export * from './classes.security-block-rule.doc.js';
|
||||||
|
export * from './classes.security-policy-audit.doc.js';
|
||||||
|
|
||||||
// Config document classes
|
// Config document classes
|
||||||
export * from './classes.route.doc.js';
|
export * from './classes.route.doc.js';
|
||||||
|
|||||||
@@ -57,6 +57,31 @@ export class EmailDomainManager {
|
|||||||
return doc ? this.docToInterface(doc) : null;
|
return doc ? this.docToInterface(doc) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getByDomain(domainName: string): Promise<IEmailDomain | null> {
|
||||||
|
const doc = await EmailDomainDoc.findByDomain(domainName);
|
||||||
|
return doc ? this.docToInterface(doc) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ensureEmailDomainForDomainName(domainName: string): Promise<IEmailDomain | null> {
|
||||||
|
const normalizedDomain = domainName.trim().toLowerCase();
|
||||||
|
const existing = await this.getByDomain(normalizedDomain);
|
||||||
|
if (existing) return existing;
|
||||||
|
if (this.isDomainAlreadyConfigured(normalizedDomain)) return null;
|
||||||
|
|
||||||
|
const linkedDomain = await this.findLinkedDnsDomain(normalizedDomain);
|
||||||
|
if (!linkedDomain) {
|
||||||
|
throw new Error(`DNS domain not found for email domain: ${normalizedDomain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subdomain = normalizedDomain === linkedDomain.name
|
||||||
|
? undefined
|
||||||
|
: normalizedDomain.slice(0, -(linkedDomain.name.length + 1));
|
||||||
|
return await this.createEmailDomain({
|
||||||
|
linkedDomainId: linkedDomain.id,
|
||||||
|
subdomain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async createEmailDomain(opts: {
|
public async createEmailDomain(opts: {
|
||||||
linkedDomainId: string;
|
linkedDomainId: string;
|
||||||
subdomain?: string;
|
subdomain?: string;
|
||||||
@@ -351,6 +376,13 @@ export class EmailDomainManager {
|
|||||||
return configuredDomains.includes(domainName.toLowerCase());
|
return configuredDomains.includes(domainName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async findLinkedDnsDomain(domainName: string): Promise<DomainDoc | null> {
|
||||||
|
const domains = await DomainDoc.findAll();
|
||||||
|
return domains
|
||||||
|
.filter((domainDoc) => domainName === domainDoc.name || domainName.endsWith(`.${domainDoc.name}`))
|
||||||
|
.sort((a, b) => b.name.length - a.name.length)[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
private async buildManagedDomainConfigs(): Promise<IEmailDomainConfig[]> {
|
private async buildManagedDomainConfigs(): Promise<IEmailDomainConfig[]> {
|
||||||
const docs = await EmailDomainDoc.findAll();
|
const docs = await EmailDomainDoc.findAll();
|
||||||
const managedConfigs: IEmailDomainConfig[] = [];
|
const managedConfigs: IEmailDomainConfig[] = [];
|
||||||
@@ -378,7 +410,7 @@ export class EmailDomainManager {
|
|||||||
return managedConfigs;
|
return managedConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncManagedDomainsToRuntime(): Promise<void> {
|
public async syncManagedDomainsToRuntime(): Promise<void> {
|
||||||
if (!this.dcRouter.options?.emailConfig) {
|
if (!this.dcRouter.options?.emailConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import type {
|
||||||
|
IEmailRoute,
|
||||||
|
IUnifiedEmailServerOptions,
|
||||||
|
} from '@push.rocks/smartmta';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as interfaces from '../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
type TSyncRequest = interfaces.requests.IReq_SyncWorkAppMailIdentity['request'];
|
||||||
|
|
||||||
|
interface IStoredWorkAppMailIdentity extends interfaces.data.IWorkAppMailIdentity {
|
||||||
|
smtpPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStoredWorkAppMailState {
|
||||||
|
version: 1;
|
||||||
|
identities: IStoredWorkAppMailIdentity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkAppMailManager {
|
||||||
|
private readonly storageKey = '/workhosters/mail-identities.json';
|
||||||
|
|
||||||
|
constructor(private dcRouterRef: any) {}
|
||||||
|
|
||||||
|
public async listMailIdentities(
|
||||||
|
ownership?: Partial<interfaces.data.IWorkAppMailOwnership>,
|
||||||
|
): Promise<interfaces.data.IWorkAppMailIdentity[]> {
|
||||||
|
const identities = await this.readStoredIdentities();
|
||||||
|
return identities
|
||||||
|
.filter((identity) => this.matchesOwnership(identity.ownership, ownership))
|
||||||
|
.map((identity) => this.toPublicIdentity(identity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async syncMailIdentity(
|
||||||
|
request: TSyncRequest,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<interfaces.data.IWorkAppMailIdentitySyncResult> {
|
||||||
|
if (!this.dcRouterRef.options.emailConfig) {
|
||||||
|
return { success: false, message: 'Email server is not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownership = this.normalizeOwnership(request.ownership);
|
||||||
|
const domain = this.normalizeDomain(request.domain);
|
||||||
|
const localPart = this.normalizeLocalPart(request.localPart);
|
||||||
|
const address = `${localPart}@${domain}`;
|
||||||
|
const externalKey = this.buildExternalKey(ownership, address);
|
||||||
|
const identities = await this.readStoredIdentities();
|
||||||
|
const existingIndex = identities.findIndex((identity) => identity.externalKey === externalKey);
|
||||||
|
|
||||||
|
if (request.delete) {
|
||||||
|
if (existingIndex < 0) {
|
||||||
|
return { success: true, action: 'unchanged' };
|
||||||
|
}
|
||||||
|
const [deletedIdentity] = identities.splice(existingIndex, 1);
|
||||||
|
await this.writeStoredIdentities(identities);
|
||||||
|
await this.applyStoredIdentitiesToRuntime(identities);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
action: 'deleted',
|
||||||
|
identity: this.toPublicIdentity(deletedIdentity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ensureEmailDomainConfigured(domain);
|
||||||
|
|
||||||
|
const existingIdentity = existingIndex >= 0 ? identities[existingIndex] : undefined;
|
||||||
|
const now = Date.now();
|
||||||
|
const smtpPassword = existingIdentity && !request.resetSmtpPassword
|
||||||
|
? existingIdentity.smtpPassword
|
||||||
|
: this.generateSmtpPassword();
|
||||||
|
const identity: IStoredWorkAppMailIdentity = {
|
||||||
|
id: existingIdentity?.id || plugins.smartunique.shortId(),
|
||||||
|
externalKey,
|
||||||
|
ownership,
|
||||||
|
address,
|
||||||
|
localPart,
|
||||||
|
domain,
|
||||||
|
enabled: request.enabled ?? existingIdentity?.enabled ?? true,
|
||||||
|
displayName: request.displayName ?? existingIdentity?.displayName,
|
||||||
|
inbound: this.normalizeInboundRoute(request.inbound ?? existingIdentity?.inbound),
|
||||||
|
smtp: {
|
||||||
|
enabled: request.smtpEnabled ?? existingIdentity?.smtp.enabled ?? true,
|
||||||
|
username: existingIdentity?.smtp.username || this.buildSmtpUsername(externalKey),
|
||||||
|
},
|
||||||
|
createdAt: existingIdentity?.createdAt || now,
|
||||||
|
updatedAt: now,
|
||||||
|
createdBy: existingIdentity?.createdBy || createdBy,
|
||||||
|
smtpPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
identities[existingIndex] = identity;
|
||||||
|
} else {
|
||||||
|
identities.push(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.writeStoredIdentities(identities);
|
||||||
|
await this.applyStoredIdentitiesToRuntime(identities);
|
||||||
|
|
||||||
|
const response: interfaces.data.IWorkAppMailIdentitySyncResult = {
|
||||||
|
success: true,
|
||||||
|
action: existingIndex >= 0 ? 'updated' : 'created',
|
||||||
|
identity: this.toPublicIdentity(identity),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex < 0 || request.resetSmtpPassword) {
|
||||||
|
response.smtpCredentials = this.buildSmtpCredentials(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async applyStoredIdentitiesToEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
||||||
|
emailConfig: TConfig,
|
||||||
|
): Promise<TConfig> {
|
||||||
|
const identities = await this.readStoredIdentities();
|
||||||
|
return this.mergeIdentitiesIntoEmailConfig(emailConfig, identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async applyStoredIdentitiesToRuntime(
|
||||||
|
identities = undefined as IStoredWorkAppMailIdentity[] | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
|
||||||
|
if (!emailConfig) return;
|
||||||
|
|
||||||
|
const nextConfig = this.mergeIdentitiesIntoEmailConfig(
|
||||||
|
emailConfig,
|
||||||
|
identities || await this.readStoredIdentities(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dcRouterRef.options.emailConfig = nextConfig;
|
||||||
|
if (this.dcRouterRef.emailServer) {
|
||||||
|
this.dcRouterRef.emailServer.updateOptions({ auth: nextConfig.auth });
|
||||||
|
await this.dcRouterRef.updateEmailRoutes(nextConfig.routes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
|
||||||
|
const storedData = await this.dcRouterRef.storageManager.get(this.storageKey);
|
||||||
|
if (!storedData) return [];
|
||||||
|
const parsed = JSON.parse(storedData) as IStoredWorkAppMailState | IStoredWorkAppMailIdentity[];
|
||||||
|
return Array.isArray(parsed) ? parsed : parsed.identities || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeStoredIdentities(identities: IStoredWorkAppMailIdentity[]): Promise<void> {
|
||||||
|
const state: IStoredWorkAppMailState = {
|
||||||
|
version: 1,
|
||||||
|
identities,
|
||||||
|
};
|
||||||
|
await this.dcRouterRef.storageManager.set(this.storageKey, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeIdentitiesIntoEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
||||||
|
emailConfig: TConfig,
|
||||||
|
identities: IStoredWorkAppMailIdentity[],
|
||||||
|
): TConfig {
|
||||||
|
const generatedRoutes = identities
|
||||||
|
.filter((identity) => identity.enabled && identity.inbound?.enabled)
|
||||||
|
.map((identity) => this.buildInboundRoute(identity));
|
||||||
|
const configuredRoutes = (emailConfig.routes || [])
|
||||||
|
.filter((route) => !this.isManagedMailRouteName(route.name));
|
||||||
|
const generatedUsers = identities
|
||||||
|
.filter((identity) => identity.enabled && identity.smtp.enabled)
|
||||||
|
.map((identity) => ({
|
||||||
|
username: identity.smtp.username,
|
||||||
|
password: identity.smtpPassword,
|
||||||
|
}));
|
||||||
|
const configuredUsers = (emailConfig.auth?.users || [])
|
||||||
|
.filter((user) => !this.isManagedSmtpUsername(user.username));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...emailConfig,
|
||||||
|
routes: [...configuredRoutes, ...generatedRoutes],
|
||||||
|
auth: {
|
||||||
|
...(emailConfig.auth || {}),
|
||||||
|
users: [...configuredUsers, ...generatedUsers],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
|
||||||
|
const inbound = identity.inbound!;
|
||||||
|
return {
|
||||||
|
name: this.buildRouteName(identity.externalKey),
|
||||||
|
priority: 1000,
|
||||||
|
match: {
|
||||||
|
recipients: identity.address,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
forward: {
|
||||||
|
host: inbound.targetHost,
|
||||||
|
port: inbound.targetPort,
|
||||||
|
preserveHeaders: inbound.preserveHeaders ?? true,
|
||||||
|
addHeaders: {
|
||||||
|
'X-Dcrouter-WorkHoster-Type': identity.ownership.workHosterType,
|
||||||
|
'X-Dcrouter-WorkHoster-Id': identity.ownership.workHosterId,
|
||||||
|
'X-Dcrouter-WorkApp-Id': identity.ownership.workAppId,
|
||||||
|
...(inbound.addHeaders || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureEmailDomainConfigured(domain: string): Promise<void> {
|
||||||
|
const emailConfig = this.dcRouterRef.options.emailConfig as IUnifiedEmailServerOptions | undefined;
|
||||||
|
if (emailConfig?.domains?.some((domainConfig) => domainConfig.domain.toLowerCase() === domain)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailDomainManager = this.dcRouterRef.emailDomainManager;
|
||||||
|
if (!emailDomainManager) {
|
||||||
|
throw new Error(`Email domain is not configured: ${domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await emailDomainManager.getByDomain(domain)) {
|
||||||
|
await emailDomainManager.syncManagedDomainsToRuntime();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await emailDomainManager.ensureEmailDomainForDomainName(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeOwnership(
|
||||||
|
ownership: interfaces.data.IWorkAppMailOwnership,
|
||||||
|
): interfaces.data.IWorkAppMailOwnership {
|
||||||
|
const workHosterType = ownership.workHosterType;
|
||||||
|
const workHosterId = ownership.workHosterId?.trim();
|
||||||
|
const workAppId = ownership.workAppId?.trim();
|
||||||
|
if (!['onebox', 'cloudly', 'custom'].includes(workHosterType)) {
|
||||||
|
throw new Error(`Invalid WorkHoster type: ${workHosterType}`);
|
||||||
|
}
|
||||||
|
if (!workHosterId) throw new Error('workHosterId is required');
|
||||||
|
if (!workAppId) throw new Error('workAppId is required');
|
||||||
|
return { workHosterType, workHosterId, workAppId };
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeDomain(domain: string): string {
|
||||||
|
const normalized = domain?.trim().toLowerCase();
|
||||||
|
if (!normalized || normalized.includes('@') || !normalized.includes('.')) {
|
||||||
|
throw new Error(`Invalid email domain: ${domain}`);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeLocalPart(localPart: string): string {
|
||||||
|
const normalized = localPart?.trim().toLowerCase();
|
||||||
|
if (!normalized || normalized.includes('@') || /\s/.test(normalized)) {
|
||||||
|
throw new Error(`Invalid email local part: ${localPart}`);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeInboundRoute(
|
||||||
|
inbound?: interfaces.data.IWorkAppMailInboundRoute,
|
||||||
|
): interfaces.data.IWorkAppMailInboundRoute | undefined {
|
||||||
|
if (!inbound) return undefined;
|
||||||
|
if (!inbound.enabled) {
|
||||||
|
return { ...inbound, enabled: false };
|
||||||
|
}
|
||||||
|
const targetHost = inbound.targetHost?.trim();
|
||||||
|
const targetPort = Number(inbound.targetPort);
|
||||||
|
if (!targetHost) throw new Error('inbound.targetHost is required when inbound routing is enabled');
|
||||||
|
if (!Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535) {
|
||||||
|
throw new Error(`Invalid inbound.targetPort: ${inbound.targetPort}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...inbound,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchesOwnership(
|
||||||
|
ownership: interfaces.data.IWorkAppMailOwnership,
|
||||||
|
filter?: Partial<interfaces.data.IWorkAppMailOwnership>,
|
||||||
|
): boolean {
|
||||||
|
if (!filter) return true;
|
||||||
|
if (filter.workHosterType && filter.workHosterType !== ownership.workHosterType) return false;
|
||||||
|
if (filter.workHosterId && filter.workHosterId !== ownership.workHosterId) return false;
|
||||||
|
if (filter.workAppId && filter.workAppId !== ownership.workAppId) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildExternalKey(
|
||||||
|
ownership: interfaces.data.IWorkAppMailOwnership,
|
||||||
|
address: string,
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
ownership.workHosterType,
|
||||||
|
ownership.workHosterId,
|
||||||
|
ownership.workAppId,
|
||||||
|
address,
|
||||||
|
].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSmtpUsername(externalKey: string): string {
|
||||||
|
return `workapp-${this.hashExternalKey(externalKey).slice(0, 24)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRouteName(externalKey: string): string {
|
||||||
|
return `workapp-mail-${this.hashExternalKey(externalKey).slice(0, 32)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashExternalKey(externalKey: string): string {
|
||||||
|
return plugins.crypto.createHash('sha256').update(externalKey).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSmtpPassword(): string {
|
||||||
|
return plugins.crypto.randomBytes(24).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isManagedMailRouteName(routeName: string): boolean {
|
||||||
|
return routeName.startsWith('workapp-mail-');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isManagedSmtpUsername(username: string): boolean {
|
||||||
|
return username.startsWith('workapp-');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSmtpCredentials(
|
||||||
|
identity: IStoredWorkAppMailIdentity,
|
||||||
|
): interfaces.data.IWorkAppMailCredentials {
|
||||||
|
return {
|
||||||
|
username: identity.smtp.username,
|
||||||
|
password: identity.smtpPassword,
|
||||||
|
host: this.dcRouterRef.options.emailConfig?.outbound?.hostname
|
||||||
|
|| this.dcRouterRef.options.emailConfig?.hostname,
|
||||||
|
ports: {
|
||||||
|
smtp: this.dcRouterRef.options.emailConfig?.ports?.includes(25) ? 25 : undefined,
|
||||||
|
submission: this.dcRouterRef.options.emailConfig?.ports?.includes(587) ? 587 : undefined,
|
||||||
|
smtps: this.dcRouterRef.options.emailConfig?.ports?.includes(465) ? 465 : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toPublicIdentity(
|
||||||
|
identity: IStoredWorkAppMailIdentity,
|
||||||
|
): interfaces.data.IWorkAppMailIdentity {
|
||||||
|
const { smtpPassword, ...publicIdentity } = identity;
|
||||||
|
return publicIdentity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './classes.email-domain.manager.js';
|
export * from './classes.email-domain.manager.js';
|
||||||
export * from './classes.smartmta-storage-manager.js';
|
export * from './classes.smartmta-storage-manager.js';
|
||||||
|
export * from './classes.workapp-mail-manager.js';
|
||||||
export * from './email-dns-records.js';
|
export * from './email-dns-records.js';
|
||||||
|
|||||||
@@ -725,6 +725,8 @@ export class MetricsManager {
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
||||||
|
|
||||||
|
void this.dcRouter.securityPolicyManager?.observeIps([...allIPData.keys()]);
|
||||||
|
|
||||||
// Build domain activity using per-IP domain request counts from Rust engine
|
// Build domain activity using per-IP domain request counts from Rust engine
|
||||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||||
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class OpsServer {
|
|||||||
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
||||||
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
||||||
private emailDomainHandler!: handlers.EmailDomainHandler;
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
||||||
|
private workHosterHandler!: handlers.WorkHosterHandler;
|
||||||
|
|
||||||
constructor(dcRouterRefArg: DcRouter) {
|
constructor(dcRouterRefArg: DcRouter) {
|
||||||
this.dcRouterRef = dcRouterRefArg;
|
this.dcRouterRef = dcRouterRefArg;
|
||||||
@@ -106,6 +107,7 @@ export class OpsServer {
|
|||||||
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
||||||
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
||||||
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
||||||
|
this.workHosterHandler = new handlers.WorkHosterHandler(this);
|
||||||
|
|
||||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,21 +26,51 @@ export function deriveCertDomainName(domain: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CertificateHandler {
|
export class CertificateHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter?.addTypedRouter(this.typedrouter);
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private async requireAuth(
|
||||||
const viewRouter = this.opsServerRef.viewRouter;
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
||||||
const adminRouter = this.opsServerRef.adminRouter;
|
requiredScope?: interfaces.data.TApiTokenScope,
|
||||||
|
): Promise<string> {
|
||||||
|
if (request.identity?.jwt) {
|
||||||
|
try {
|
||||||
|
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
||||||
|
identity: request.identity,
|
||||||
|
});
|
||||||
|
if (isAdmin) return request.identity.userId;
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
|
if (request.apiToken) {
|
||||||
|
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (tokenManager) {
|
||||||
|
const token = await tokenManager.validateToken(request.apiToken);
|
||||||
|
if (token) {
|
||||||
|
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
||||||
|
return token.createdBy;
|
||||||
|
}
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
const router = this.typedrouter;
|
||||||
|
|
||||||
// Get Certificate Overview
|
// Get Certificate Overview
|
||||||
viewRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
||||||
'getCertificateOverview',
|
'getCertificateOverview',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:read');
|
||||||
const certificates = await this.buildCertificateOverview();
|
const certificates = await this.buildCertificateOverview();
|
||||||
const summary = this.buildSummary(certificates);
|
const summary = this.buildSummary(certificates);
|
||||||
return { certificates, summary };
|
return { certificates, summary };
|
||||||
@@ -48,53 +78,56 @@ export class CertificateHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
|
|
||||||
|
|
||||||
// Legacy route-based reprovision (backward compat)
|
// Legacy route-based reprovision (backward compat)
|
||||||
adminRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||||
'reprovisionCertificate',
|
'reprovisionCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:write');
|
||||||
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Domain-based reprovision (preferred)
|
// Domain-based reprovision (preferred)
|
||||||
adminRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||||
'reprovisionCertificateDomain',
|
'reprovisionCertificateDomain',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:write');
|
||||||
return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
|
return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete certificate
|
// Delete certificate
|
||||||
adminRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
||||||
'deleteCertificate',
|
'deleteCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:write');
|
||||||
return this.deleteCertificate(dataArg.domain);
|
return this.deleteCertificate(dataArg.domain);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Export certificate
|
// Export certificate
|
||||||
adminRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
||||||
'exportCertificate',
|
'exportCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:read');
|
||||||
return this.exportCertificate(dataArg.domain);
|
return this.exportCertificate(dataArg.domain);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Import certificate
|
// Import certificate
|
||||||
adminRouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
||||||
'importCertificate',
|
'importCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'certificates:write');
|
||||||
return this.importCertificate(dataArg.cert);
|
return this.importCertificate(dataArg.cert);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ export * from './domain.handler.js';
|
|||||||
export * from './dns-record.handler.js';
|
export * from './dns-record.handler.js';
|
||||||
export * from './acme-config.handler.js';
|
export * from './acme-config.handler.js';
|
||||||
export * from './email-domain.handler.js';
|
export * from './email-domain.handler.js';
|
||||||
|
export * from './workhoster.handler.js';
|
||||||
|
|||||||
@@ -157,6 +157,113 @@ export class SecurityHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>(
|
||||||
|
'listSecurityBlockRules',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { rules: manager ? await manager.listBlockRules() : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>(
|
||||||
|
'listIpIntelligence',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { records: manager ? await manager.listIpIntelligence() : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCompiledSecurityPolicy>(
|
||||||
|
'getCompiledSecurityPolicy',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return {
|
||||||
|
policy: manager
|
||||||
|
? await manager.compilePolicy()
|
||||||
|
: { blockedIps: [], blockedCidrs: [] },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityPolicyAudit>(
|
||||||
|
'listSecurityPolicyAudit',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { events: manager ? await manager.listAuditEvents(dataArg.limit || 100) : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>(
|
||||||
|
'createSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const rule = await manager.createBlockRule({
|
||||||
|
type: dataArg.type,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
}, dataArg.identity.userId);
|
||||||
|
return { success: true, rule };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>(
|
||||||
|
'updateSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const rule = await manager.updateBlockRule(dataArg.id, {
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
}, dataArg.identity.userId);
|
||||||
|
return rule ? { success: true, rule } : { success: false, message: 'Rule not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>(
|
||||||
|
'deleteSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const success = await manager.deleteBlockRule(dataArg.id, dataArg.identity.userId);
|
||||||
|
return { success, message: success ? undefined : 'Rule not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshIpIntelligence>(
|
||||||
|
'refreshIpIntelligence',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const record = await manager.refreshIpIntelligence(dataArg.ipAddress);
|
||||||
|
return record
|
||||||
|
? { success: true, record }
|
||||||
|
: { success: false, message: 'IP address is invalid or not public' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectSecurityMetrics(): Promise<{
|
private async collectSecurityMetrics(): Promise<{
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class WorkHosterHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requireAuth(
|
||||||
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
||||||
|
requiredScope?: interfaces.data.TApiTokenScope,
|
||||||
|
): Promise<string> {
|
||||||
|
if (request.identity?.jwt) {
|
||||||
|
try {
|
||||||
|
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
||||||
|
identity: request.identity,
|
||||||
|
});
|
||||||
|
if (isAdmin) return request.identity.userId;
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.apiToken) {
|
||||||
|
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (tokenManager) {
|
||||||
|
const token = await tokenManager.validateToken(request.apiToken);
|
||||||
|
if (token) {
|
||||||
|
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
||||||
|
return token.createdBy;
|
||||||
|
}
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayCapabilities>(
|
||||||
|
'getGatewayCapabilities',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'workhosters:read');
|
||||||
|
return { capabilities: this.getGatewayCapabilities() };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkHosterDomains>(
|
||||||
|
'getWorkHosterDomains',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'workhosters:read');
|
||||||
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
|
if (!dnsManager) return { domains: [] };
|
||||||
|
|
||||||
|
const docs = await dnsManager.listDomains();
|
||||||
|
const domains = docs.map((domainDoc) => {
|
||||||
|
const domain = dnsManager.toPublicDomain(domainDoc);
|
||||||
|
const canManageDnsRecords = domain.source === 'dcrouter' || Boolean(domain.providerId);
|
||||||
|
return {
|
||||||
|
...domain,
|
||||||
|
capabilities: {
|
||||||
|
canCreateSubdomains: canManageDnsRecords,
|
||||||
|
canManageDnsRecords,
|
||||||
|
canIssueCertificates: Boolean(this.opsServerRef.dcRouterRef.smartProxy),
|
||||||
|
canHostEmail: Boolean(this.opsServerRef.dcRouterRef.emailDomainManager),
|
||||||
|
},
|
||||||
|
} satisfies interfaces.data.IWorkHosterDomain;
|
||||||
|
});
|
||||||
|
return { domains };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppRoute>(
|
||||||
|
'syncWorkAppRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'workhosters:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const externalKey = this.buildExternalKey(dataArg.ownership);
|
||||||
|
const existingRoute = manager.findApiRouteByExternalKey(externalKey);
|
||||||
|
|
||||||
|
if (dataArg.delete) {
|
||||||
|
if (!existingRoute) {
|
||||||
|
return { success: true, action: 'unchanged' };
|
||||||
|
}
|
||||||
|
const result = await manager.deleteRoute(existingRoute.id);
|
||||||
|
return result.success
|
||||||
|
? { success: true, action: 'deleted', routeId: existingRoute.id }
|
||||||
|
: { success: false, message: result.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataArg.route) {
|
||||||
|
return { success: false, message: 'route is required unless delete=true' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata: interfaces.data.IRouteMetadata = {
|
||||||
|
ownerType: 'workhoster',
|
||||||
|
workHosterType: dataArg.ownership.workHosterType,
|
||||||
|
workHosterId: dataArg.ownership.workHosterId,
|
||||||
|
workAppId: dataArg.ownership.workAppId,
|
||||||
|
externalKey,
|
||||||
|
};
|
||||||
|
const route = this.normalizeWorkAppRoute(dataArg.route, dataArg.ownership, externalKey);
|
||||||
|
|
||||||
|
if (existingRoute) {
|
||||||
|
const result = await manager.updateRoute(existingRoute.id, {
|
||||||
|
route,
|
||||||
|
enabled: dataArg.enabled ?? true,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
return result.success
|
||||||
|
? { success: true, action: 'updated', routeId: existingRoute.id }
|
||||||
|
: { success: false, message: result.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeId = await manager.createRoute(route, userId, dataArg.enabled ?? true, metadata);
|
||||||
|
return { success: true, action: 'created', routeId };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkAppMailIdentities>(
|
||||||
|
'getWorkAppMailIdentities',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'workhosters:read');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
||||||
|
if (!manager) return { identities: [] };
|
||||||
|
return { identities: await manager.listMailIdentities(dataArg.ownership) };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppMailIdentity>(
|
||||||
|
'syncWorkAppMailIdentity',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'workhosters:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'WorkApp mail manager not initialized' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await manager.syncMailIdentity(dataArg, userId);
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: (error as Error).message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGatewayCapabilities(): interfaces.data.IGatewayCapabilities {
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
return {
|
||||||
|
routes: {
|
||||||
|
read: Boolean(dcRouter.routeConfigManager),
|
||||||
|
write: Boolean(dcRouter.routeConfigManager),
|
||||||
|
idempotentSync: Boolean(dcRouter.routeConfigManager),
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
read: Boolean(dcRouter.dnsManager),
|
||||||
|
write: Boolean(dcRouter.dnsManager),
|
||||||
|
},
|
||||||
|
certificates: {
|
||||||
|
read: Boolean(dcRouter.smartProxy),
|
||||||
|
export: Boolean(dcRouter.smartProxy),
|
||||||
|
forceRenew: Boolean(dcRouter.smartProxy),
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
domains: Boolean(dcRouter.emailDomainManager),
|
||||||
|
inbound: Boolean(dcRouter.emailServer),
|
||||||
|
outbound: Boolean(dcRouter.emailServer),
|
||||||
|
},
|
||||||
|
remoteIngress: {
|
||||||
|
enabled: Boolean(dcRouter.options.remoteIngressConfig?.enabled),
|
||||||
|
},
|
||||||
|
dns: {
|
||||||
|
authoritative: Boolean(dcRouter.options.dnsScopes?.length),
|
||||||
|
providerManaged: Boolean(dcRouter.dnsManager),
|
||||||
|
},
|
||||||
|
http3: {
|
||||||
|
enabled: dcRouter.options.http3?.enabled !== false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string {
|
||||||
|
return [
|
||||||
|
ownership.workHosterType,
|
||||||
|
ownership.workHosterId,
|
||||||
|
ownership.workAppId,
|
||||||
|
ownership.hostname,
|
||||||
|
].map((part) => part.trim()).join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeWorkAppRoute(
|
||||||
|
route: interfaces.data.IDcRouterRouteConfig,
|
||||||
|
ownership: interfaces.data.IWorkAppRouteOwnership,
|
||||||
|
externalKey: string,
|
||||||
|
): interfaces.data.IDcRouterRouteConfig {
|
||||||
|
const normalizedRoute = { ...route };
|
||||||
|
if (!normalizedRoute.name) {
|
||||||
|
normalizedRoute.name = `workapp-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
||||||
|
}
|
||||||
|
return normalizedRoute;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ import * as plugins from '../plugins.js';
|
|||||||
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||||
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
||||||
|
|
||||||
|
interface IRemoteIngressFirewallConfig {
|
||||||
|
blockedIps?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +35,7 @@ function extractPorts(portRange: number | Array<number | { from: number; to: num
|
|||||||
export class RemoteIngressManager {
|
export class RemoteIngressManager {
|
||||||
private edges: Map<string, IRemoteIngress> = new Map();
|
private edges: Map<string, IRemoteIngress> = new Map();
|
||||||
private routes: IDcRouterRouteConfig[] = [];
|
private routes: IDcRouterRouteConfig[] = [];
|
||||||
|
private firewallConfig?: IRemoteIngressFirewallConfig;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,13 @@ export class RemoteIngressManager {
|
|||||||
this.routes = routes;
|
this.routes = routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the full desired firewall snapshot pushed to all edges.
|
||||||
|
*/
|
||||||
|
public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void {
|
||||||
|
this.firewallConfig = firewallConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
|
* 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.
|
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
|
||||||
@@ -305,8 +317,8 @@ export class RemoteIngressManager {
|
|||||||
* Get the list of allowed edges (enabled only) for the Rust hub.
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||||
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
||||||
*/
|
*/
|
||||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
|
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
|
||||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
|
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
|
||||||
for (const edge of this.edges.values()) {
|
for (const edge of this.edges.values()) {
|
||||||
if (edge.enabled) {
|
if (edge.enabled) {
|
||||||
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||||
@@ -315,6 +327,7 @@ export class RemoteIngressManager {
|
|||||||
secret: edge.secret,
|
secret: edge.secret,
|
||||||
listenPorts: this.getEffectiveListenPorts(edge),
|
listenPorts: this.getEffectiveListenPorts(edge),
|
||||||
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||||
|
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
|
||||||
|
import type {
|
||||||
|
IIpIntelligenceRecord,
|
||||||
|
ISecurityBlockRule,
|
||||||
|
ISecurityCompiledPolicy,
|
||||||
|
ISecurityPolicyAuditEvent,
|
||||||
|
TSecurityBlockRuleMatchMode,
|
||||||
|
TSecurityBlockRuleType,
|
||||||
|
} from '../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
export interface ISecurityPolicyManagerOptions {
|
||||||
|
intelligenceRefreshMs?: number;
|
||||||
|
onPolicyChanged?: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressFirewallSnapshot {
|
||||||
|
blockedIps: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecurityPolicyManager {
|
||||||
|
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
|
||||||
|
cacheTtl: 24 * 60 * 60 * 1000,
|
||||||
|
});
|
||||||
|
private readonly intelligenceRefreshMs: number;
|
||||||
|
private readonly inFlightObservations = new Set<string>();
|
||||||
|
private readonly onPolicyChanged?: () => void | Promise<void>;
|
||||||
|
|
||||||
|
constructor(options: ISecurityPolicyManagerOptions = {}) {
|
||||||
|
this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
|
||||||
|
this.onPolicyChanged = options.onPolicyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
logger.log('info', 'SecurityPolicyManager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await this.smartNetwork.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async observeIps(ips: string[]): Promise<void> {
|
||||||
|
const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
|
||||||
|
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
|
||||||
|
const ip = this.normalizeIp(ipAddress);
|
||||||
|
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inFlightObservations.add(ip);
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
let doc = await IpIntelligenceDoc.findByIp(ip);
|
||||||
|
if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
||||||
|
if (now - doc.lastSeenAt > 60_000) {
|
||||||
|
doc.lastSeenAt = now;
|
||||||
|
doc.seenCount = (doc.seenCount || 0) + 1;
|
||||||
|
await doc.save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intelligence = await this.smartNetwork.getIpIntelligence(ip);
|
||||||
|
if (!doc) {
|
||||||
|
doc = new IpIntelligenceDoc();
|
||||||
|
doc.ipAddress = ip;
|
||||||
|
doc.firstSeenAt = now;
|
||||||
|
}
|
||||||
|
Object.assign(doc, intelligence);
|
||||||
|
doc.lastSeenAt = now;
|
||||||
|
doc.updatedAt = now;
|
||||||
|
doc.seenCount = (doc.seenCount || 0) + 1;
|
||||||
|
await doc.save();
|
||||||
|
|
||||||
|
if (await this.matchesAnyReactiveRule(doc)) {
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
this.inFlightObservations.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listBlockRules(): Promise<ISecurityBlockRule[]> {
|
||||||
|
return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
|
||||||
|
return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
|
||||||
|
const ip = this.normalizeIp(ipAddress);
|
||||||
|
if (!ip || !this.isPublicIp(ip)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await this.observeIp(ip, { force: true });
|
||||||
|
const doc = await IpIntelligenceDoc.findByIp(ip);
|
||||||
|
return doc ? this.intelligenceFromDoc(doc) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
|
||||||
|
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
action: doc.action,
|
||||||
|
actor: doc.actor,
|
||||||
|
details: doc.details,
|
||||||
|
createdAt: doc.createdAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private intelligenceFromDoc(doc: IpIntelligenceDoc): IIpIntelligenceRecord {
|
||||||
|
return {
|
||||||
|
ipAddress: doc.ipAddress,
|
||||||
|
asn: doc.asn,
|
||||||
|
asnOrg: doc.asnOrg,
|
||||||
|
registrantOrg: doc.registrantOrg,
|
||||||
|
registrantCountry: doc.registrantCountry,
|
||||||
|
networkRange: doc.networkRange,
|
||||||
|
networkCidrs: doc.networkCidrs,
|
||||||
|
abuseContact: doc.abuseContact,
|
||||||
|
country: doc.country,
|
||||||
|
countryCode: doc.countryCode,
|
||||||
|
city: doc.city,
|
||||||
|
latitude: doc.latitude,
|
||||||
|
longitude: doc.longitude,
|
||||||
|
accuracyRadius: doc.accuracyRadius,
|
||||||
|
timezone: doc.timezone,
|
||||||
|
firstSeenAt: doc.firstSeenAt,
|
||||||
|
lastSeenAt: doc.lastSeenAt,
|
||||||
|
updatedAt: doc.updatedAt,
|
||||||
|
seenCount: doc.seenCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createBlockRule(input: {
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}, actor = 'system'): Promise<ISecurityBlockRule> {
|
||||||
|
const now = Date.now();
|
||||||
|
const doc = new SecurityBlockRuleDoc();
|
||||||
|
doc.id = plugins.uuid.v4();
|
||||||
|
doc.type = input.type;
|
||||||
|
doc.value = input.value.trim();
|
||||||
|
doc.matchMode = input.matchMode;
|
||||||
|
doc.reason = input.reason;
|
||||||
|
doc.enabled = input.enabled ?? true;
|
||||||
|
doc.createdAt = now;
|
||||||
|
doc.updatedAt = now;
|
||||||
|
doc.createdBy = actor;
|
||||||
|
await doc.save();
|
||||||
|
await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return this.ruleFromDoc(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
|
||||||
|
const doc = await SecurityBlockRuleDoc.findById(id);
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (patch.value !== undefined) doc.value = patch.value.trim();
|
||||||
|
if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
|
||||||
|
if (patch.reason !== undefined) doc.reason = patch.reason;
|
||||||
|
if (patch.enabled !== undefined) doc.enabled = patch.enabled;
|
||||||
|
doc.updatedAt = Date.now();
|
||||||
|
await doc.save();
|
||||||
|
await this.writeAudit('updateBlockRule', actor, { id, patch });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return this.ruleFromDoc(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
|
||||||
|
const doc = await SecurityBlockRuleDoc.findById(id);
|
||||||
|
if (!doc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await doc.delete();
|
||||||
|
await this.writeAudit('deleteBlockRule', actor, { id });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
|
||||||
|
const rules = await SecurityBlockRuleDoc.findEnabled();
|
||||||
|
const intelligenceDocs = await IpIntelligenceDoc.findAll();
|
||||||
|
const blockedIps = new Set<string>();
|
||||||
|
const blockedCidrs = new Set<string>();
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const normalizedValue = rule.value.trim();
|
||||||
|
if (!normalizedValue) continue;
|
||||||
|
|
||||||
|
if (rule.type === 'ip') {
|
||||||
|
const ip = this.normalizeIp(normalizedValue);
|
||||||
|
if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'cidr') {
|
||||||
|
for (const cidr of this.normalizeNetworkEntries(normalizedValue)) {
|
||||||
|
blockedCidrs.add(cidr);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const doc of intelligenceDocs) {
|
||||||
|
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
|
||||||
|
const networkEntries = this.normalizeNetworkEntryList([
|
||||||
|
...(doc.networkCidrs || []),
|
||||||
|
doc.networkRange,
|
||||||
|
]);
|
||||||
|
if (networkEntries.length > 0) {
|
||||||
|
for (const cidr of networkEntries) {
|
||||||
|
blockedCidrs.add(cidr);
|
||||||
|
}
|
||||||
|
} else if (this.normalizeIp(doc.ipAddress)) {
|
||||||
|
blockedIps.add(this.normalizeIp(doc.ipAddress)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedIps: [...blockedIps].sort(),
|
||||||
|
blockedCidrs: [...blockedCidrs].sort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
|
||||||
|
return await this.compilePolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot> {
|
||||||
|
const policy = await this.compilePolicy();
|
||||||
|
const blockedIps = [
|
||||||
|
...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
|
||||||
|
...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
|
||||||
|
];
|
||||||
|
return { blockedIps };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
|
||||||
|
const rules = await SecurityBlockRuleDoc.findEnabled();
|
||||||
|
return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
|
||||||
|
? this.ruleMatchesIntelligence(rule, doc)
|
||||||
|
: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
|
||||||
|
const value = rule.value.trim().toLowerCase();
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
if (rule.type === 'asn') {
|
||||||
|
return String(doc.asn ?? '') === value.replace(/^as/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'organization') {
|
||||||
|
const candidates = [doc.asnOrg, doc.registrantOrg]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((candidate) => candidate!.toLowerCase());
|
||||||
|
if (rule.matchMode === 'exact') {
|
||||||
|
return candidates.some((candidate) => candidate === value);
|
||||||
|
}
|
||||||
|
return candidates.some((candidate) => candidate.includes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeIp(ipAddress: string): string | undefined {
|
||||||
|
const ip = ipAddress.trim();
|
||||||
|
if (ip.startsWith('::ffff:')) {
|
||||||
|
return ip.slice('::ffff:'.length);
|
||||||
|
}
|
||||||
|
return plugins.net.isIP(ip) ? ip : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeCidr(value: string): string | undefined {
|
||||||
|
const [rawIp, rawPrefix] = value.trim().split('/');
|
||||||
|
if (!rawIp || !rawPrefix) return undefined;
|
||||||
|
const ip = this.normalizeIp(rawIp);
|
||||||
|
if (!ip) return undefined;
|
||||||
|
const prefix = Number(rawPrefix);
|
||||||
|
const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
|
||||||
|
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
|
||||||
|
return `${ip}/${prefix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeNetworkEntries(value: string): string[] {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return [];
|
||||||
|
|
||||||
|
const cidr = this.normalizeCidr(trimmed);
|
||||||
|
if (cidr) return [cidr];
|
||||||
|
|
||||||
|
const rangeParts = trimmed.split(/\s+-\s+/);
|
||||||
|
if (rangeParts.length === 2) {
|
||||||
|
return this.ipv4RangeToCidrs(rangeParts[0], rangeParts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeNetworkEntryList(values: Array<string | null | undefined>): string[] {
|
||||||
|
const cidrs = new Set<string>();
|
||||||
|
for (const value of values) {
|
||||||
|
if (!value) continue;
|
||||||
|
for (const entry of value.split(',').map((part) => part.trim()).filter(Boolean)) {
|
||||||
|
for (const cidr of this.normalizeNetworkEntries(entry)) {
|
||||||
|
cidrs.add(cidr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...cidrs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
|
||||||
|
const start = this.ipv4ToBigInt(startIp);
|
||||||
|
const end = this.ipv4ToBigInt(endIp);
|
||||||
|
if (start === undefined || end === undefined || start > end) return [];
|
||||||
|
|
||||||
|
const cidrs: string[] = [];
|
||||||
|
let current = start;
|
||||||
|
while (current <= end) {
|
||||||
|
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
|
||||||
|
const remaining = end - current + 1n;
|
||||||
|
while (maxBlockSize > remaining) {
|
||||||
|
maxBlockSize = maxBlockSize / 2n;
|
||||||
|
}
|
||||||
|
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
|
||||||
|
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
|
||||||
|
current += maxBlockSize;
|
||||||
|
}
|
||||||
|
return cidrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ipv4ToBigInt(ip: string): bigint | undefined {
|
||||||
|
const normalized = this.normalizeIp(ip);
|
||||||
|
if (!normalized || plugins.net.isIP(normalized) !== 4) return undefined;
|
||||||
|
return normalized
|
||||||
|
.split('.')
|
||||||
|
.reduce((sum, part) => (sum * 256n) + BigInt(Number(part)), 0n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private numberToIpv4(value: bigint): string {
|
||||||
|
return [
|
||||||
|
Number((value >> 24n) & 255n),
|
||||||
|
Number((value >> 16n) & 255n),
|
||||||
|
Number((value >> 8n) & 255n),
|
||||||
|
Number(value & 255n),
|
||||||
|
].join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private powerOfTwoExponent(value: bigint): number {
|
||||||
|
let exponent = 0;
|
||||||
|
let remaining = value;
|
||||||
|
while (remaining > 1n) {
|
||||||
|
remaining >>= 1n;
|
||||||
|
exponent++;
|
||||||
|
}
|
||||||
|
return exponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPublicIp(ip: string): boolean {
|
||||||
|
const family = plugins.net.isIP(ip);
|
||||||
|
if (family === 4) {
|
||||||
|
const parts = ip.split('.').map((part) => Number(part));
|
||||||
|
const [a, b] = parts;
|
||||||
|
if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return false;
|
||||||
|
if (a === 169 && b === 254) return false;
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return false;
|
||||||
|
if (a === 192 && b === 168) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (family === 6) {
|
||||||
|
const lower = ip.toLowerCase();
|
||||||
|
if (lower === '::1' || lower === '::') return false;
|
||||||
|
if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
type: doc.type,
|
||||||
|
value: doc.value,
|
||||||
|
matchMode: doc.matchMode,
|
||||||
|
enabled: doc.enabled,
|
||||||
|
reason: doc.reason,
|
||||||
|
createdAt: doc.createdAt,
|
||||||
|
updatedAt: doc.updatedAt,
|
||||||
|
createdBy: doc.createdBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
|
||||||
|
const doc = new SecurityPolicyAuditDoc();
|
||||||
|
doc.id = plugins.uuid.v4();
|
||||||
|
doc.action = action;
|
||||||
|
doc.actor = actor;
|
||||||
|
doc.details = details;
|
||||||
|
doc.createdAt = Date.now();
|
||||||
|
await doc.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyPolicyChanged(): Promise<void> {
|
||||||
|
if (this.onPolicyChanged) {
|
||||||
|
await this.onPolicyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,3 +19,9 @@ export {
|
|||||||
type IScanResult,
|
type IScanResult,
|
||||||
type IContentScannerOptions
|
type IContentScannerOptions
|
||||||
} from './classes.contentscanner.js';
|
} from './classes.contentscanner.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SecurityPolicyManager,
|
||||||
|
type ISecurityPolicyManagerOptions,
|
||||||
|
type IRemoteIngressFirewallSnapshot,
|
||||||
|
} from './classes.security-policy-manager.js';
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ConfigManager } from './classes.config.js';
|
|||||||
import { LogManager } from './classes.logs.js';
|
import { LogManager } from './classes.logs.js';
|
||||||
import { EmailManager } from './classes.email.js';
|
import { EmailManager } from './classes.email.js';
|
||||||
import { RadiusManager } from './classes.radius.js';
|
import { RadiusManager } from './classes.radius.js';
|
||||||
|
import { WorkHosterManager } from './classes.workhoster.js';
|
||||||
|
|
||||||
export interface IDcRouterApiClientOptions {
|
export interface IDcRouterApiClientOptions {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -31,6 +32,7 @@ export class DcRouterApiClient {
|
|||||||
public logs: LogManager;
|
public logs: LogManager;
|
||||||
public emails: EmailManager;
|
public emails: EmailManager;
|
||||||
public radius: RadiusManager;
|
public radius: RadiusManager;
|
||||||
|
public workHosters: WorkHosterManager;
|
||||||
|
|
||||||
constructor(options: IDcRouterApiClientOptions) {
|
constructor(options: IDcRouterApiClientOptions) {
|
||||||
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
||||||
@@ -45,6 +47,7 @@ export class DcRouterApiClient {
|
|||||||
this.logs = new LogManager(this);
|
this.logs = new LogManager(this);
|
||||||
this.emails = new EmailManager(this);
|
this.emails = new EmailManager(this);
|
||||||
this.radius = new RadiusManager(this);
|
this.radius = new RadiusManager(this);
|
||||||
|
this.workHosters = new WorkHosterManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class WorkHosterManager {
|
||||||
|
constructor(private clientRef: DcRouterApiClient) {}
|
||||||
|
|
||||||
|
public async getCapabilities(): Promise<interfaces.data.IGatewayCapabilities> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetGatewayCapabilities>(
|
||||||
|
'getGatewayCapabilities',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDomains(): Promise<interfaces.data.IWorkHosterDomain[]> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetWorkHosterDomains>(
|
||||||
|
'getWorkHosterDomains',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async syncRoute(options: {
|
||||||
|
ownership: interfaces.data.IWorkAppRouteOwnership;
|
||||||
|
route: interfaces.data.IDcRouterRouteConfig;
|
||||||
|
enabled?: boolean;
|
||||||
|
}): Promise<interfaces.data.IWorkAppRouteSyncResult> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_SyncWorkAppRoute>(
|
||||||
|
'syncWorkAppRoute',
|
||||||
|
this.clientRef.buildRequestPayload({
|
||||||
|
ownership: options.ownership,
|
||||||
|
route: options.route,
|
||||||
|
enabled: options.enabled,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRoute(
|
||||||
|
ownership: interfaces.data.IWorkAppRouteOwnership,
|
||||||
|
): Promise<interfaces.data.IWorkAppRouteSyncResult> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_SyncWorkAppRoute>(
|
||||||
|
'syncWorkAppRoute',
|
||||||
|
this.clientRef.buildRequestPayload({
|
||||||
|
ownership,
|
||||||
|
delete: true,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export { Certificate, CertificateManager, type ICertificateSummary } from './cla
|
|||||||
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
|
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
|
||||||
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
|
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
|
||||||
export { Email, EmailManager } from './classes.email.js';
|
export { Email, EmailManager } from './classes.email.js';
|
||||||
|
export { WorkHosterManager } from './classes.workhoster.js';
|
||||||
|
|
||||||
// Read-only managers
|
// Read-only managers
|
||||||
export { StatsManager } from './classes.stats.js';
|
export { StatsManager } from './classes.stats.js';
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export * from './target-profile.js';
|
|||||||
export * from './vpn.js';
|
export * from './vpn.js';
|
||||||
export * from './dns-provider.js';
|
export * from './dns-provider.js';
|
||||||
export * from './domain.js';
|
export * from './domain.js';
|
||||||
|
export * from './workhoster.js';
|
||||||
export * from './dns-record.js';
|
export * from './dns-record.js';
|
||||||
export * from './acme-config.js';
|
export * from './acme-config.js';
|
||||||
export * from './email-domain.js';
|
export * from './email-domain.js';
|
||||||
|
export * from './security-policy.js';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type IRouteSecurity = NonNullable<IRouteConfig['security']>;
|
|||||||
export type TApiTokenScope =
|
export type TApiTokenScope =
|
||||||
| 'routes:read' | 'routes:write'
|
| 'routes:read' | 'routes:write'
|
||||||
| 'config:read'
|
| 'config:read'
|
||||||
|
| 'certificates:read' | 'certificates:write'
|
||||||
| 'tokens:read' | 'tokens:manage'
|
| 'tokens:read' | 'tokens:manage'
|
||||||
| 'source-profiles:read' | 'source-profiles:write'
|
| 'source-profiles:read' | 'source-profiles:write'
|
||||||
| 'target-profiles:read' | 'target-profiles:write'
|
| 'target-profiles:read' | 'target-profiles:write'
|
||||||
@@ -18,7 +19,11 @@ export type TApiTokenScope =
|
|||||||
| 'dns-providers:read' | 'dns-providers:write'
|
| 'dns-providers:read' | 'dns-providers:write'
|
||||||
| 'domains:read' | 'domains:write'
|
| 'domains:read' | 'domains:write'
|
||||||
| 'dns-records:read' | 'dns-records:write'
|
| 'dns-records:read' | 'dns-records:write'
|
||||||
| 'acme-config:read' | 'acme-config:write';
|
| 'acme-config:read' | 'acme-config:write'
|
||||||
|
| 'email-domains:read' | 'email-domains:write'
|
||||||
|
| 'workhosters:read' | 'workhosters:write';
|
||||||
|
|
||||||
|
export type TWorkHosterType = 'onebox' | 'cloudly' | 'custom';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Source Profile Types (source-side: who can access)
|
// Source Profile Types (source-side: who can access)
|
||||||
@@ -80,6 +85,12 @@ export interface IRouteMetadata {
|
|||||||
networkTargetName?: string;
|
networkTargetName?: string;
|
||||||
/** Timestamp of last reference resolution. */
|
/** Timestamp of last reference resolution. */
|
||||||
lastResolvedAt?: number;
|
lastResolvedAt?: number;
|
||||||
|
/** External route ownership, used by WorkHoster reconciliation. */
|
||||||
|
ownerType?: 'workhoster' | 'operator' | 'system';
|
||||||
|
workHosterType?: TWorkHosterType;
|
||||||
|
workHosterId?: string;
|
||||||
|
workAppId?: string;
|
||||||
|
externalKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { IIpIntelligenceResult } from '@push.rocks/smartnetwork';
|
||||||
|
|
||||||
|
export type TSecurityBlockRuleType = 'ip' | 'cidr' | 'asn' | 'organization';
|
||||||
|
export type TSecurityBlockRuleMatchMode = 'exact' | 'contains';
|
||||||
|
|
||||||
|
export interface IIpIntelligenceRecord extends IIpIntelligenceResult {
|
||||||
|
ipAddress: string;
|
||||||
|
firstSeenAt: number;
|
||||||
|
lastSeenAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
seenCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityBlockRule {
|
||||||
|
id: string;
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
enabled: boolean;
|
||||||
|
reason?: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityCompiledPolicy {
|
||||||
|
blockedIps: string[];
|
||||||
|
blockedCidrs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityPolicyAuditEvent {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
actor: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import type { IDomain } from './domain.js';
|
||||||
|
|
||||||
|
export interface IGatewayCapabilities {
|
||||||
|
routes: {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
idempotentSync: boolean;
|
||||||
|
};
|
||||||
|
domains: {
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
};
|
||||||
|
certificates: {
|
||||||
|
read: boolean;
|
||||||
|
export: boolean;
|
||||||
|
forceRenew: boolean;
|
||||||
|
};
|
||||||
|
email: {
|
||||||
|
domains: boolean;
|
||||||
|
inbound: boolean;
|
||||||
|
outbound: boolean;
|
||||||
|
};
|
||||||
|
remoteIngress: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
dns: {
|
||||||
|
authoritative: boolean;
|
||||||
|
providerManaged: boolean;
|
||||||
|
};
|
||||||
|
http3: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkHosterDomain extends IDomain {
|
||||||
|
capabilities: {
|
||||||
|
canCreateSubdomains: boolean;
|
||||||
|
canManageDnsRecords: boolean;
|
||||||
|
canIssueCertificates: boolean;
|
||||||
|
canHostEmail: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppRouteOwnership {
|
||||||
|
workHosterType: 'onebox' | 'cloudly' | 'custom';
|
||||||
|
workHosterId: string;
|
||||||
|
workAppId: string;
|
||||||
|
hostname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppRouteSyncResult {
|
||||||
|
success: boolean;
|
||||||
|
action?: 'created' | 'updated' | 'deleted' | 'unchanged';
|
||||||
|
routeId?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppMailOwnership {
|
||||||
|
workHosterType: 'onebox' | 'cloudly' | 'custom';
|
||||||
|
workHosterId: string;
|
||||||
|
workAppId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppMailInboundRoute {
|
||||||
|
enabled: boolean;
|
||||||
|
targetHost: string;
|
||||||
|
targetPort: number;
|
||||||
|
preserveHeaders?: boolean;
|
||||||
|
addHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppMailIdentity {
|
||||||
|
id: string;
|
||||||
|
externalKey: string;
|
||||||
|
ownership: IWorkAppMailOwnership;
|
||||||
|
address: string;
|
||||||
|
localPart: string;
|
||||||
|
domain: string;
|
||||||
|
enabled: boolean;
|
||||||
|
displayName?: string;
|
||||||
|
inbound?: IWorkAppMailInboundRoute;
|
||||||
|
smtp: {
|
||||||
|
enabled: boolean;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppMailCredentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
host?: string;
|
||||||
|
ports?: {
|
||||||
|
smtp?: number;
|
||||||
|
submission?: number;
|
||||||
|
smtps?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkAppMailIdentitySyncResult {
|
||||||
|
success: boolean;
|
||||||
|
action?: 'created' | 'updated' | 'deleted' | 'unchanged';
|
||||||
|
identity?: IWorkAppMailIdentity;
|
||||||
|
smtpCredentials?: IWorkAppMailCredentials;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
|
|||||||
> {
|
> {
|
||||||
method: 'getCertificateOverview';
|
method: 'getCertificateOverview';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
certificates: ICertificateInfo[];
|
certificates: ICertificateInfo[];
|
||||||
@@ -50,7 +51,8 @@ export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfa
|
|||||||
> {
|
> {
|
||||||
method: 'reprovisionCertificate';
|
method: 'reprovisionCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
routeName: string;
|
routeName: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -66,7 +68,8 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
|
|||||||
> {
|
> {
|
||||||
method: 'reprovisionCertificateDomain';
|
method: 'reprovisionCertificateDomain';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
forceRenew?: boolean;
|
forceRenew?: boolean;
|
||||||
};
|
};
|
||||||
@@ -83,7 +86,8 @@ export interface IReq_DeleteCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'deleteCertificate';
|
method: 'deleteCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -99,7 +103,8 @@ export interface IReq_ExportCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'exportCertificate';
|
method: 'exportCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -124,7 +129,8 @@ export interface IReq_ImportCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'importCertificate';
|
method: 'importCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
cert: {
|
cert: {
|
||||||
id: string;
|
id: string;
|
||||||
domainName: string;
|
domainName: string;
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ export * from './domains.js';
|
|||||||
export * from './dns-records.js';
|
export * from './dns-records.js';
|
||||||
export * from './acme-config.js';
|
export * from './acme-config.js';
|
||||||
export * from './email-domains.js';
|
export * from './email-domains.js';
|
||||||
|
export * from './workhoster.js';
|
||||||
|
export * from './security-policy.js';
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type {
|
||||||
|
IIpIntelligenceRecord,
|
||||||
|
ISecurityBlockRule,
|
||||||
|
ISecurityCompiledPolicy,
|
||||||
|
ISecurityPolicyAuditEvent,
|
||||||
|
TSecurityBlockRuleMatchMode,
|
||||||
|
TSecurityBlockRuleType,
|
||||||
|
} from '../data/security-policy.js';
|
||||||
|
|
||||||
|
export interface IReq_ListSecurityBlockRules extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListSecurityBlockRules
|
||||||
|
> {
|
||||||
|
method: 'listSecurityBlockRules';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
rules: ISecurityBlockRule[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_CreateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'createSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
rule?: ISecurityBlockRule;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_UpdateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'updateSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
rule?: ISecurityBlockRule;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_DeleteSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'deleteSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListIpIntelligence
|
||||||
|
> {
|
||||||
|
method: 'listIpIntelligence';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
records: IIpIntelligenceRecord[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetCompiledSecurityPolicy extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetCompiledSecurityPolicy
|
||||||
|
> {
|
||||||
|
method: 'getCompiledSecurityPolicy';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
policy: ISecurityCompiledPolicy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_ListSecurityPolicyAudit extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListSecurityPolicyAudit
|
||||||
|
> {
|
||||||
|
method: 'listSecurityPolicyAudit';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
events: ISecurityPolicyAuditEvent[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_RefreshIpIntelligence extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RefreshIpIntelligence
|
||||||
|
> {
|
||||||
|
method: 'refreshIpIntelligence';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
ipAddress: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
record?: IIpIntelligenceRecord;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type {
|
||||||
|
IGatewayCapabilities,
|
||||||
|
IWorkAppMailIdentity,
|
||||||
|
IWorkAppMailIdentitySyncResult,
|
||||||
|
IWorkAppMailInboundRoute,
|
||||||
|
IWorkAppMailOwnership,
|
||||||
|
IWorkAppRouteOwnership,
|
||||||
|
IWorkAppRouteSyncResult,
|
||||||
|
IWorkHosterDomain,
|
||||||
|
} from '../data/workhoster.js';
|
||||||
|
import type { IDcRouterRouteConfig } from '../data/remoteingress.js';
|
||||||
|
|
||||||
|
export interface IReq_GetGatewayCapabilities extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetGatewayCapabilities
|
||||||
|
> {
|
||||||
|
method: 'getGatewayCapabilities';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
capabilities: IGatewayCapabilities;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetWorkHosterDomains extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetWorkHosterDomains
|
||||||
|
> {
|
||||||
|
method: 'getWorkHosterDomains';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domains: IWorkHosterDomain[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_SyncWorkAppRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SyncWorkAppRoute
|
||||||
|
> {
|
||||||
|
method: 'syncWorkAppRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
ownership: IWorkAppRouteOwnership;
|
||||||
|
route?: IDcRouterRouteConfig;
|
||||||
|
enabled?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
};
|
||||||
|
response: IWorkAppRouteSyncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetWorkAppMailIdentities extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetWorkAppMailIdentities
|
||||||
|
> {
|
||||||
|
method: 'getWorkAppMailIdentities';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
ownership?: Partial<IWorkAppMailOwnership>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
identities: IWorkAppMailIdentity[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_SyncWorkAppMailIdentity extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SyncWorkAppMailIdentity
|
||||||
|
> {
|
||||||
|
method: 'syncWorkAppMailIdentity';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
ownership: IWorkAppMailOwnership;
|
||||||
|
localPart: string;
|
||||||
|
domain: string;
|
||||||
|
displayName?: string;
|
||||||
|
inbound?: IWorkAppMailInboundRoute;
|
||||||
|
enabled?: boolean;
|
||||||
|
smtpEnabled?: boolean;
|
||||||
|
resetSmtpPassword?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
};
|
||||||
|
response: IWorkAppMailIdentitySyncResult;
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.22.0',
|
version: '13.25.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+236
-3
@@ -54,6 +54,7 @@ export interface INetworkState {
|
|||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
||||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
|
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
||||||
domainActivity: interfaces.data.IDomainActivity[];
|
domainActivity: interfaces.data.IDomainActivity[];
|
||||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond: number;
|
requestsPerSecond: number;
|
||||||
@@ -66,6 +67,16 @@ export interface INetworkState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISecurityPolicyState {
|
||||||
|
rules: interfaces.data.ISecurityBlockRule[];
|
||||||
|
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
||||||
|
compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
|
||||||
|
auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICertificateState {
|
export interface ICertificateState {
|
||||||
certificates: interfaces.requests.ICertificateInfo[];
|
certificates: interfaces.requests.ICertificateInfo[];
|
||||||
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
||||||
@@ -164,6 +175,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
topIPs: [],
|
topIPs: [],
|
||||||
topIPsByBandwidth: [],
|
topIPsByBandwidth: [],
|
||||||
throughputByIP: [],
|
throughputByIP: [],
|
||||||
|
ipIntelligence: [],
|
||||||
domainActivity: [],
|
domainActivity: [],
|
||||||
throughputHistory: [],
|
throughputHistory: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
@@ -178,6 +190,20 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
|
||||||
|
'securityPolicy',
|
||||||
|
{
|
||||||
|
rules: [],
|
||||||
|
ipIntelligence: [],
|
||||||
|
compiledPolicy: null,
|
||||||
|
auditEvents: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
|
'soft',
|
||||||
|
);
|
||||||
|
|
||||||
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||||
'emailOps',
|
'emailOps',
|
||||||
{
|
{
|
||||||
@@ -517,9 +543,18 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
interfaces.requests.IReq_GetNetworkStats
|
interfaces.requests.IReq_GetNetworkStats
|
||||||
>('/typedrequest', 'getNetworkStats');
|
>('/typedrequest', 'getNetworkStats');
|
||||||
|
|
||||||
const networkStatsResponse = await networkStatsRequest.fire({
|
const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
identity: context.identity,
|
interfaces.requests.IReq_ListIpIntelligence
|
||||||
});
|
>('/typedrequest', 'listIpIntelligence');
|
||||||
|
|
||||||
|
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
|
||||||
|
networkStatsRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
}),
|
||||||
|
ipIntelligenceRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
// Use the connections data for the connection list
|
// Use the connections data for the connection list
|
||||||
// and network stats for throughput and IP analytics
|
// and network stats for throughput and IP analytics
|
||||||
@@ -561,6 +596,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
topIPs: networkStatsResponse.topIPs || [],
|
topIPs: networkStatsResponse.topIPs || [],
|
||||||
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
||||||
throughputByIP: networkStatsResponse.throughputByIP || [],
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||||
|
ipIntelligence: ipIntelligenceResponse.records || [],
|
||||||
domainActivity: networkStatsResponse.domainActivity || [],
|
domainActivity: networkStatsResponse.domainActivity || [],
|
||||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||||
@@ -582,6 +618,182 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Security Policy Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
||||||
|
async (statePartArg): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListSecurityBlockRules
|
||||||
|
>('/typedrequest', 'listSecurityBlockRules');
|
||||||
|
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListIpIntelligence
|
||||||
|
>('/typedrequest', 'listIpIntelligence');
|
||||||
|
const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetCompiledSecurityPolicy
|
||||||
|
>('/typedrequest', 'getCompiledSecurityPolicy');
|
||||||
|
const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListSecurityPolicyAudit
|
||||||
|
>('/typedrequest', 'listSecurityPolicyAudit');
|
||||||
|
|
||||||
|
const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
|
||||||
|
rulesRequest.fire({ identity: context.identity }),
|
||||||
|
intelligenceRequest.fire({ identity: context.identity }),
|
||||||
|
compiledPolicyRequest.fire({ identity: context.identity }),
|
||||||
|
auditRequest.fire({ identity: context.identity, limit: 100 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: rulesResponse.rules || [],
|
||||||
|
ipIntelligence: intelligenceResponse.records || [],
|
||||||
|
compiledPolicy: compiledPolicyResponse.policy,
|
||||||
|
auditEvents: auditResponse.events || [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch security policy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
||||||
|
type: interfaces.data.TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateSecurityBlockRule
|
||||||
|
>('/typedrequest', 'createSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
type: dataArg.type,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to create security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateSecurityBlockRule
|
||||||
|
>('/typedrequest', 'updateSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to update security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
|
||||||
|
async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_DeleteSecurityBlockRule
|
||||||
|
>('/typedrequest', 'deleteSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({ identity: context.identity, id: ruleId });
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to delete security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
|
||||||
|
async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RefreshIpIntelligence
|
||||||
|
>('/typedrequest', 'refreshIpIntelligence');
|
||||||
|
const response = await request.fire({ identity: context.identity, ipAddress });
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
||||||
|
}
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Email Operations Actions
|
// Email Operations Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -2665,6 +2877,27 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListIpIntelligence
|
||||||
|
>('/typedrequest', 'listIpIntelligence');
|
||||||
|
const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity });
|
||||||
|
networkStatePart.setState({
|
||||||
|
...networkStatePart.getState()!,
|
||||||
|
ipIntelligence: intelligenceResponse.records || [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('IP intelligence refresh failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentView === 'security') {
|
||||||
|
try {
|
||||||
|
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Security policy refresh failed:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh certificate data if on Domains > Certificates subview
|
// Refresh certificate data if on Domains > Certificates subview
|
||||||
|
|||||||
@@ -199,12 +199,22 @@ export class OpsViewApiTokens extends DeesElement {
|
|||||||
private async showCreateTokenDialog() {
|
private async showCreateTokenDialog() {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
const allScopes: TApiTokenScope[] = [
|
const allScopes = [
|
||||||
'routes:read',
|
'routes:read',
|
||||||
'routes:write',
|
'routes:write',
|
||||||
'config:read',
|
'config:read',
|
||||||
|
'certificates:read',
|
||||||
|
'certificates:write',
|
||||||
'tokens:read',
|
'tokens:read',
|
||||||
'tokens:manage',
|
'tokens:manage',
|
||||||
|
'domains:read',
|
||||||
|
'domains:write',
|
||||||
|
'dns-records:read',
|
||||||
|
'dns-records:write',
|
||||||
|
'email-domains:read',
|
||||||
|
'email-domains:write',
|
||||||
|
'workhosters:read',
|
||||||
|
'workhosters:write',
|
||||||
];
|
];
|
||||||
|
|
||||||
await DeesModal.createAndShow({
|
await DeesModal.createAndShow({
|
||||||
|
|||||||
@@ -255,6 +255,17 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.intelligenceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
|
||||||
|
color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
|
||||||
|
}
|
||||||
|
|
||||||
.protocolChartGrid {
|
.protocolChartGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@@ -345,6 +356,100 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatOptional(value: unknown): string {
|
||||||
|
if (value === null || value === undefined || value === '') return '-';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDateTime(timestamp?: number | null): string {
|
||||||
|
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligence(ip: string): interfaces.data.IIpIntelligenceRecord | undefined {
|
||||||
|
return this.networkState.ipIntelligence?.find((record) => record.ipAddress === ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpOrganization(record?: interfaces.data.IIpIntelligenceRecord): string {
|
||||||
|
return record?.asnOrg || record?.registrantOrg || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligenceColumns(ip: string): Record<string, unknown> {
|
||||||
|
const record = this.getIpIntelligence(ip);
|
||||||
|
const organization = this.getIpOrganization(record);
|
||||||
|
return {
|
||||||
|
'Intelligence': record
|
||||||
|
? html`<span class="intelligenceBadge">${this.formatOptional(organization || record.countryCode || 'Known')}</span>`
|
||||||
|
: html`<span class="statusBadge warning">Enriching...</span>`,
|
||||||
|
'ASN': record?.asn ? `AS${record.asn}` : '-',
|
||||||
|
'Organization': this.formatOptional(organization),
|
||||||
|
'Country': this.formatOptional(record?.countryCode || record?.country),
|
||||||
|
'Network Range': this.formatOptional(record?.networkRange),
|
||||||
|
'Last Seen': this.formatDateTime(record?.lastSeenAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpDataActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Refresh Intelligence',
|
||||||
|
iconName: 'lucide:refresh-cw',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const ip = actionData.item.ip;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, ip);
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
await this.createBlockRuleDialog('ip', actionData.item.ip, 'Blocked from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Network Range',
|
||||||
|
iconName: 'lucide:network',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.networkRange),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('cidr', record!.networkRange!, 'Blocked network range from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block ASN',
|
||||||
|
iconName: 'lucide:radio-tower',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.asn),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('asn', String(record!.asn), 'Blocked ASN from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Organization',
|
||||||
|
iconName: 'lucide:building-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpOrganization(this.getIpIntelligence(actionData.item.ip))),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('organization', this.getIpOrganization(record), 'Blocked organization from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View Intelligence',
|
||||||
|
iconName: 'lucide:info',
|
||||||
|
type: ['doubleClick', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
await this.showIpIntelligenceDetails(actionData.item.ip);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private calculateThroughput(): { in: number; out: number } {
|
private calculateThroughput(): { in: number; out: number } {
|
||||||
// Use real throughput data from network state
|
// Use real throughput data from network state
|
||||||
return {
|
return {
|
||||||
@@ -500,10 +605,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||||
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||||
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||||
|
...this.getIpIntelligenceColumns(ipData.ip),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
.dataActions=${this.getIpDataActions()}
|
||||||
heading1="Top Connected IPs"
|
heading1="Top Connected IPs"
|
||||||
heading2="IPs with most active connections and bandwidth"
|
heading2="IPs with most active connections, bandwidth, and intelligence"
|
||||||
searchable
|
searchable
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
@@ -529,10 +636,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
||||||
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
||||||
'Connections': ipData.count,
|
'Connections': ipData.count,
|
||||||
|
...this.getIpIntelligenceColumns(ipData.ip),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
.dataActions=${this.getIpDataActions()}
|
||||||
heading1="Top IPs by Bandwidth"
|
heading1="Top IPs by Bandwidth"
|
||||||
heading2="IPs with highest throughput"
|
heading2="IPs with highest throughput and intelligence"
|
||||||
searchable
|
searchable
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
@@ -678,6 +787,114 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDropdownKey(value: any): string {
|
||||||
|
return typeof value === 'string' ? value : value?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createBlockRuleDialog(
|
||||||
|
type: interfaces.data.TSecurityBlockRuleType,
|
||||||
|
value: string,
|
||||||
|
reason: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const typeOptions = [
|
||||||
|
{ key: 'ip', option: 'IP address' },
|
||||||
|
{ key: 'cidr', option: 'CIDR / network range' },
|
||||||
|
{ key: 'asn', option: 'ASN' },
|
||||||
|
{ key: 'organization', option: 'Organization' },
|
||||||
|
];
|
||||||
|
const matchModeOptions = [
|
||||||
|
{ key: 'contains', option: 'Organization contains value' },
|
||||||
|
{ key: 'exact', option: 'Organization exactly matches value' },
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Create Security Block Rule',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Rule Type'}
|
||||||
|
.options=${typeOptions}
|
||||||
|
.selectedOption=${typeOptions.find((option) => option.key === type)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'value'} .label=${'Value'} .value=${value} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'matchMode'}
|
||||||
|
.label=${'Organization Match Mode'}
|
||||||
|
.description=${'Only used for organization rules'}
|
||||||
|
.options=${matchModeOptions}
|
||||||
|
.selectedOption=${matchModeOptions[0]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'reason'} .label=${'Reason'} .value=${reason}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'enabled'} .label=${'Enable immediately'} .value=${true}></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const data = await form.collectFormData();
|
||||||
|
const selectedType = this.getDropdownKey(data.type) as interfaces.data.TSecurityBlockRuleType;
|
||||||
|
const selectedValue = String(data.value || '').trim();
|
||||||
|
if (!selectedType || !selectedValue) return;
|
||||||
|
const matchMode = selectedType === 'organization'
|
||||||
|
? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
|
||||||
|
: undefined;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
|
||||||
|
type: selectedType,
|
||||||
|
value: selectedValue,
|
||||||
|
matchMode,
|
||||||
|
reason: String(data.reason || '').trim() || undefined,
|
||||||
|
enabled: data.enabled !== false,
|
||||||
|
});
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showIpIntelligenceDetails(ip: string): Promise<void> {
|
||||||
|
const record = this.getIpIntelligence(ip);
|
||||||
|
if (!record) return;
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `IP Intelligence: ${ip}`,
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<dees-dataview-codebox
|
||||||
|
.heading=${'Intelligence Record'}
|
||||||
|
progLang="json"
|
||||||
|
.codeToDisplay=${JSON.stringify(record, null, 2)}
|
||||||
|
></dees-dataview-codebox>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy Abuse Contact',
|
||||||
|
iconName: 'lucide:copy',
|
||||||
|
action: async () => {
|
||||||
|
if (record.abuseContact) await navigator.clipboard.writeText(record.abuseContact);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
action: async () => {
|
||||||
|
await this.createBlockRuleDialog('ip', record.ipAddress, 'Blocked from IP intelligence details');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async updateNetworkData() {
|
private async updateNetworkData() {
|
||||||
// Track requests/sec history for the trend sparkline (moved out of render)
|
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from '../shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -21,18 +22,23 @@ declare global {
|
|||||||
@customElement('ops-view-security-blocked')
|
@customElement('ops-view-security-blocked')
|
||||||
export class OpsViewSecurityBlocked extends DeesElement {
|
export class OpsViewSecurityBlocked extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const sub = appstate.statsStatePart
|
const sub = appstate.securityPolicyStatePart
|
||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.statsState = s;
|
this.securityPolicyState = s;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(sub);
|
this.rxSubscriptions.push(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.fetchSecurityPolicyAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
viewHostCss,
|
viewHostCss,
|
||||||
@@ -40,79 +46,436 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|||||||
dees-statsgrid {
|
dees-statsgrid {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sectionStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.enabled {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||||
|
color: ${cssManager.bdTheme('#757575', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
|
||||||
|
color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||||
|
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const metrics = this.statsState.securityMetrics;
|
const state = this.securityPolicyState;
|
||||||
|
const activeRules = state.rules.filter((rule) => rule.enabled);
|
||||||
if (!metrics) {
|
const disabledRules = state.rules.length - activeRules.length;
|
||||||
return html`
|
const compiledPolicy = state.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||||
<div class="loadingMessage">
|
|
||||||
<p>Loading security metrics...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockedIPs: string[] = metrics.blockedIPs || [];
|
|
||||||
|
|
||||||
const tiles: IStatsTile[] = [
|
const tiles: IStatsTile[] = [
|
||||||
{
|
{
|
||||||
id: 'totalBlocked',
|
id: 'activeRules',
|
||||||
title: 'Blocked IPs',
|
title: 'Active Rules',
|
||||||
value: blockedIPs.length,
|
value: activeRules.length,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lucide:ShieldBan',
|
icon: 'lucide:shield-check',
|
||||||
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
|
color: activeRules.length > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Currently blocked addresses',
|
description: `${disabledRules} disabled`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compiledIps',
|
||||||
|
title: 'Compiled IPs',
|
||||||
|
value: compiledPolicy.blockedIps.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:server-off',
|
||||||
|
color: '#ef4444',
|
||||||
|
description: 'Direct IP blocks enforced by SmartProxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compiledCidrs',
|
||||||
|
title: 'Compiled CIDRs',
|
||||||
|
value: compiledPolicy.blockedCidrs.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:network',
|
||||||
|
color: '#f97316',
|
||||||
|
description: 'Network ranges pushed to enforcement layers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'intelligenceRecords',
|
||||||
|
title: 'IP Intelligence',
|
||||||
|
value: state.ipIntelligence.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:radar',
|
||||||
|
color: '#6366f1',
|
||||||
|
description: 'Observed public IPs with enrichment',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-heading level="3">Blocked IPs</dees-heading>
|
<dees-heading level="3">Security Blocking</dees-heading>
|
||||||
|
|
||||||
|
${state.error ? html`<div class="errorMessage">${state.error}</div>` : html``}
|
||||||
|
|
||||||
<dees-statsgrid
|
<dees-statsgrid
|
||||||
.tiles=${tiles}
|
.tiles=${tiles}
|
||||||
.minTileWidth=${200}
|
.minTileWidth=${200}
|
||||||
></dees-statsgrid>
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
<div class="sectionStack">
|
||||||
|
${this.renderRulesTable()}
|
||||||
|
${this.renderCompiledPolicyTable()}
|
||||||
|
${this.renderIpIntelligenceTable()}
|
||||||
|
${this.renderAuditTable()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRulesTable(): TemplateResult {
|
||||||
|
return html`
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Blocked IP Addresses'}
|
.heading1=${'Managed Block Rules'}
|
||||||
.heading2=${'IPs blocked due to suspicious activity'}
|
.heading2=${'Rules compiled into SmartProxy policy and remote ingress edge firewall snapshots'}
|
||||||
.data=${blockedIPs.map((ip) => ({ ip }))}
|
.data=${this.securityPolicyState.rules}
|
||||||
.displayFunction=${(item) => ({
|
.rowKey=${'id'}
|
||||||
'IP Address': item.ip,
|
.displayFunction=${(rule: interfaces.data.ISecurityBlockRule) => ({
|
||||||
'Reason': 'Suspicious activity',
|
'Type': html`<span class="typeBadge">${rule.type}</span>`,
|
||||||
|
'Value': rule.value,
|
||||||
|
'Match': rule.type === 'organization' ? (rule.matchMode || 'contains') : '-',
|
||||||
|
'Reason': rule.reason || '-',
|
||||||
|
'Status': html`<span class="statusBadge ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? 'Enabled' : 'Disabled'}</span>`,
|
||||||
|
'Created': this.formatDateTime(rule.createdAt),
|
||||||
|
'Updated': this.formatDateTime(rule.updatedAt),
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${this.getRuleActions()}
|
||||||
{
|
searchable
|
||||||
name: 'Unblock',
|
.showColumnFilters=${true}
|
||||||
iconName: 'lucide:shield-off',
|
dataName="rule"
|
||||||
type: ['contextmenu' as const],
|
|
||||||
actionFunc: async (item) => {
|
|
||||||
await this.unblockIP(item.ip);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Clear All',
|
|
||||||
iconName: 'lucide:trash-2',
|
|
||||||
type: ['header' as const],
|
|
||||||
actionFunc: async () => {
|
|
||||||
await this.clearBlockedIPs();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
></dees-table>
|
></dees-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearBlockedIPs() {
|
private renderCompiledPolicyTable(): TemplateResult {
|
||||||
// SmartProxy manages IP blocking — not yet exposed via API
|
const policy = this.securityPolicyState.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||||
alert('Clearing blocked IPs is not yet supported from the UI.');
|
const rows = [
|
||||||
|
...policy.blockedIps.map((value) => ({ type: 'ip', value })),
|
||||||
|
...policy.blockedCidrs.map((value) => ({ type: 'cidr', value })),
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Compiled Enforcement Policy'}
|
||||||
|
.heading2=${'Concrete IPs and CIDRs currently sent to SmartProxy and remote ingress'}
|
||||||
|
.data=${rows}
|
||||||
|
.rowKey=${'value'}
|
||||||
|
.displayFunction=${(row: { type: string; value: string }) => ({
|
||||||
|
'Enforcement Type': html`<span class="typeBadge">${row.type}</span>`,
|
||||||
|
'Value': row.value,
|
||||||
|
})}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="compiled rule"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unblockIP(ip: string) {
|
private renderIpIntelligenceTable(): TemplateResult {
|
||||||
// SmartProxy manages IP blocking — not yet exposed via API
|
return html`
|
||||||
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
<dees-table
|
||||||
|
.heading1=${'Observed IP Intelligence'}
|
||||||
|
.heading2=${'Public IPs observed in network metrics and enriched for ASN / organization matching'}
|
||||||
|
.data=${this.securityPolicyState.ipIntelligence}
|
||||||
|
.rowKey=${'ipAddress'}
|
||||||
|
.displayFunction=${(record: interfaces.data.IIpIntelligenceRecord) => ({
|
||||||
|
'IP Address': record.ipAddress,
|
||||||
|
'ASN': record.asn ? `AS${record.asn}` : '-',
|
||||||
|
'ASN Org': record.asnOrg || '-',
|
||||||
|
'Registrant Org': record.registrantOrg || '-',
|
||||||
|
'Country': record.countryCode || record.country || '-',
|
||||||
|
'Network Range': record.networkRange || '-',
|
||||||
|
'Abuse Contact': record.abuseContact || '-',
|
||||||
|
'Seen': record.seenCount,
|
||||||
|
'Last Seen': this.formatDateTime(record.lastSeenAt),
|
||||||
|
})}
|
||||||
|
.dataActions=${this.getIpIntelligenceActions()}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="ip intelligence record"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAuditTable(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Policy Audit'}
|
||||||
|
.heading2=${'Recent security policy changes'}
|
||||||
|
.data=${this.securityPolicyState.auditEvents}
|
||||||
|
.rowKey=${'id'}
|
||||||
|
.displayFunction=${(event: interfaces.data.ISecurityPolicyAuditEvent) => ({
|
||||||
|
'Time': this.formatDateTime(event.createdAt),
|
||||||
|
'Action': event.action,
|
||||||
|
'Actor': event.actor,
|
||||||
|
'Details': this.formatAuditDetails(event.details),
|
||||||
|
})}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="audit event"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRuleActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Create Rule',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
type: ['header'] as any,
|
||||||
|
actionFunc: async () => this.showRuleDialog(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
iconName: 'lucide:pencil',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => this.showRuleDialog(actionData.item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Enable',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disable',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
if (!window.confirm(`Delete block rule ${rule.type}:${rule.value}?`)) return;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.deleteSecurityBlockRuleAction, rule.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligenceActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Refresh Intelligence',
|
||||||
|
iconName: 'lucide:refresh-cw',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, record.ipAddress);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'ip',
|
||||||
|
value: record.ipAddress,
|
||||||
|
reason: 'Blocked from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Network Range',
|
||||||
|
iconName: 'lucide:network',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.networkRange),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'cidr',
|
||||||
|
value: record.networkRange || '',
|
||||||
|
reason: 'Blocked network range from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block ASN',
|
||||||
|
iconName: 'lucide:radio-tower',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asn),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'asn',
|
||||||
|
value: String(record.asn),
|
||||||
|
reason: 'Blocked ASN from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Organization',
|
||||||
|
iconName: 'lucide:building-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asnOrg || actionData.item.registrantOrg),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'organization',
|
||||||
|
value: record.asnOrg || record.registrantOrg || '',
|
||||||
|
reason: 'Blocked organization from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showRuleDialog(
|
||||||
|
rule?: interfaces.data.ISecurityBlockRule,
|
||||||
|
defaults: Partial<interfaces.data.ISecurityBlockRule> = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const typeOptions = [
|
||||||
|
{ key: 'ip', option: 'IP address' },
|
||||||
|
{ key: 'cidr', option: 'CIDR / network range' },
|
||||||
|
{ key: 'asn', option: 'ASN' },
|
||||||
|
{ key: 'organization', option: 'Organization' },
|
||||||
|
];
|
||||||
|
const matchModeOptions = [
|
||||||
|
{ key: 'contains', option: 'Organization contains value' },
|
||||||
|
{ key: 'exact', option: 'Organization exactly matches value' },
|
||||||
|
];
|
||||||
|
const selectedType = rule?.type || defaults.type || 'ip';
|
||||||
|
const selectedMatchMode = rule?.matchMode || defaults.matchMode || 'contains';
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: rule ? `Edit Block Rule: ${rule.type}:${rule.value}` : 'Create Block Rule',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
${rule ? html`` : html`
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Rule Type'}
|
||||||
|
.options=${typeOptions}
|
||||||
|
.selectedOption=${typeOptions.find((option) => option.key === selectedType)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
`}
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'value'}
|
||||||
|
.label=${'Value'}
|
||||||
|
.value=${rule?.value || defaults.value || ''}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'matchMode'}
|
||||||
|
.label=${'Organization Match Mode'}
|
||||||
|
.description=${'Only used for organization rules'}
|
||||||
|
.options=${matchModeOptions}
|
||||||
|
.selectedOption=${matchModeOptions.find((option) => option.key === selectedMatchMode)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'reason'}
|
||||||
|
.label=${'Reason'}
|
||||||
|
.value=${rule?.reason || defaults.reason || ''}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'enabled'}
|
||||||
|
.label=${'Enabled'}
|
||||||
|
.value=${rule ? rule.enabled : defaults.enabled !== false}
|
||||||
|
></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
{
|
||||||
|
name: rule ? 'Save' : 'Create',
|
||||||
|
iconName: rule ? 'lucide:check' : 'lucide:plus',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const data = await form.collectFormData();
|
||||||
|
const type = (rule?.type || this.getDropdownKey(data.type)) as interfaces.data.TSecurityBlockRuleType;
|
||||||
|
const value = String(data.value || '').trim();
|
||||||
|
if (!type || !value) return;
|
||||||
|
const matchMode = type === 'organization'
|
||||||
|
? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
|
||||||
|
: undefined;
|
||||||
|
const payload = {
|
||||||
|
value,
|
||||||
|
matchMode,
|
||||||
|
reason: String(data.reason || '').trim() || undefined,
|
||||||
|
enabled: data.enabled !== false,
|
||||||
|
};
|
||||||
|
if (rule) {
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
|
||||||
|
type,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDropdownKey(value: any): string {
|
||||||
|
return typeof value === 'string' ? value : value?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDateTime(timestamp?: number): string {
|
||||||
|
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatAuditDetails(details: Record<string, unknown>): string {
|
||||||
|
const text = JSON.stringify(details);
|
||||||
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user