Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20eda1ab3e | |||
| 44f2a7f0a9 | |||
| 0195a21f30 | |||
| 4dca747386 | |||
| 7663f502fa | |||
| 104cd417d8 | |||
| 93254d5d3d | |||
| 9a3f121a9c | |||
| bef74eb1aa | |||
| 308d8e4851 | |||
| dc010dc3ae | |||
| 61d5d3b1ad | |||
| dd70790d40 | |||
| 2f8c04edc4 | |||
| 474cc328dd | |||
| 39ff159bf7 | |||
| c7fe7aeb50 | |||
| 2cf362020f | |||
| b62bad3616 | |||
| 3d372863a4 | |||
| 1045dc04fe | |||
| 89ef7597df | |||
| 0804544564 | |||
| 671e72452a | |||
| 647c705b81 | |||
| 40c3202082 | |||
| 3b91ed3d5a | |||
| 133b17f136 | |||
| efa45dfdc9 | |||
| 79b4ea6bd9 | |||
| b483412a2e | |||
| d964515ff9 | |||
| e2c453423e | |||
| c44b7d513a | |||
| 2487f77b8a | |||
| ea80ef005c | |||
| dd45b7fbe7 | |||
| ca73da7b9b | |||
| f6e1951aa2 | |||
| 76fd563e21 | |||
| ee831ea057 | |||
| a65c2ec096 | |||
| 65822278d5 | |||
| aa3955fc67 | |||
| d4605062bb | |||
| cd3f08d55f | |||
| 6d447f0086 | |||
| c7de3873d8 | |||
| 6d4e30e8a9 | |||
| 0e308b692b | |||
| 9f74b6e063 | |||
| 1d0f47f256 | |||
| 4e9301ae2a | |||
| 7e2142ce53 | |||
| 67190605a6 | |||
| 9479a07ddf | |||
| fbed56092f | |||
| 547b82b35b | |||
| 3dc63fa02e | |||
| e0154f5b70 | |||
| b268409897 | |||
| f3a9fd12c5 | |||
| ef741d84fb | |||
| b0ea97b922 | |||
| d1560811f5 | |||
| 5e872c4e6a | |||
| 3620e4549a | |||
| b32865e790 | |||
| ebe71a2a94 | |||
| 877a2ad0ee | |||
| 7be1aaedb3 | |||
| 05eb8e9723 | |||
| d95d89ea6f | |||
| 5d1b988579 | |||
| bae85eea9e | |||
| 2be7974991 | |||
| ac03b1f081 | |||
| 5ca209dd5a | |||
| 867e93b246 | |||
| aa9c4c1c28 | |||
| 207f21cb77 | |||
| 96a47ef588 | |||
| 3bac80eb41 | |||
| 19d67a644c | |||
| 341e4113bd | |||
| 81eb19a9ab | |||
| e0221c0d05 | |||
| e6a1f23c84 | |||
| f77772e1c0 | |||
| 0a706c03c4 | |||
| df5547fda0 | |||
| a677ee081c | |||
| 7339a91513 | |||
| cd27645489 | |||
| 71f2d53e45 | |||
| f74fcc68de | |||
| abcaf9c858 | |||
| c9f185dd82 | |||
| 5e3186d311 | |||
| db35c01a09 | |||
| 3cf6c84a60 | |||
| d570d5c916 | |||
| 4f6afb62f3 | |||
| e5edfdc052 | |||
| 55cbb92a00 | |||
| 928be6a5c6 | |||
| dff3e3c80d | |||
| 8db2118a78 | |||
| dc6da4ce34 | |||
| 63eb99ae5d | |||
| 6b533fba9f | |||
| d3e102b6d2 | |||
| 2bc9761d21 | |||
| d60b9fb8e2 | |||
| b2705f3e88 | |||
| 4dea263cb0 | |||
| 54593d0a9b | |||
| 225a75a81b | |||
| 72ae4e8a9b | |||
| b171590179 | |||
| c46f79914f | |||
| 914223972a | |||
| 559435d13c | |||
| 47300e4ced | |||
| a35a35f302 | |||
| 3b09587ca9 | |||
| 1cb2de2c3e | |||
| f972c38784 | |||
| 90736c3668 | |||
| f27ef5c768 | |||
| 68ecd18646 | |||
| e3d7c91705 | |||
| d3c2fa436c | |||
| 53b7da7e3f | |||
| 8e312d80c0 | |||
| c52d0ca759 | |||
| ca024345f6 | |||
| 89ea875ca8 | |||
| f15414e509 | |||
| cfaafab057 | |||
| 0aed608578 | |||
| 00a24051c9 | |||
| 310b43d1a6 | |||
| e7f56fb870 | |||
| 1f6eee20d3 | |||
| 7db7f92abd | |||
| fdf995cf61 | |||
| fa5adbbf61 | |||
| 1bdda9f501 | |||
| 39a5d6aaa6 | |||
| 1d46ec709b | |||
| 2b4bf42812 | |||
| 6faccc643b | |||
| 9f63908f7d | |||
| 5889eb5210 | |||
| 1de40c0d4e | |||
| e201efe0b4 | |||
| f8c582ee9b | |||
| bf9e85f518 | |||
| 0366ec8160 | |||
| 58bd4a0d33 | |||
| bbff76814e | |||
| 292486c33b | |||
| 2df8937d86 | |||
| 14aa1fa1d4 | |||
| 7a4214a7b8 | |||
| bd6130013c | |||
| 0f814bbcdd | |||
| 8ec94b7dae | |||
| d5dfe439c7 | |||
| aaf3c9cb1c | |||
| abde872ab2 | |||
| ca2d2b09ad | |||
| fb7d4d988b | |||
| 26e6eea5d5 | |||
| 2458dd08d8 | |||
| dee648b3bc | |||
| f4ed32cee4 | |||
| e9c72952ab | |||
| 1bd485c43e | |||
| 421a0390ba | |||
| c7f87a7c22 | |||
| 390d5c648f | |||
| ec651c1cdb | |||
| 6f82c393e7 | |||
| afdb48367b | |||
| 53526ca3ba | |||
| 07e8f4489b | |||
| 14101a09d3 | |||
| 5344d53806 | |||
| 971535926c | |||
| c13a4ae4be | |||
| e7a03c48ae | |||
| a682329a3f | |||
| c4580f9874 | |||
| b331065b8c | |||
| 4675ca3e89 | |||
| 70e2c8e17d |
@@ -6,7 +6,7 @@ on:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
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_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
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_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-dbase:npmci
|
||||
image: code.foss.global/host.today/ht-docker-node:dbase_dind
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -82,15 +82,13 @@ jobs:
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
pnpm install -g @git.zone/tsdocker
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci docker login
|
||||
npmci docker build
|
||||
npmci docker test
|
||||
# npmci docker push gitea.lossless.digital
|
||||
npmci docker push dockerregistry.lossless.digital
|
||||
tsdocker login
|
||||
tsdocker build
|
||||
tsdocker push
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"to": "./dist_serve/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"bundler": "esbuild",
|
||||
"production": true
|
||||
"production": true,
|
||||
"includeFiles": ["./html/**/*.html"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,9 +72,14 @@
|
||||
"dockerRegistryRepoMap": {
|
||||
"registry.gitlab.com": "code.foss.global/serve.zone/dcrouter"
|
||||
},
|
||||
"dockerBuildargEnvMap": {
|
||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
||||
},
|
||||
"npmRegistryUrl": "verdaccio.lossless.digital"
|
||||
},
|
||||
"@git.zone/tsdocker": {
|
||||
"registries": ["code.foss.global"],
|
||||
"registryRepoMap": {
|
||||
"code.foss.global": "serve.zone/dcrouter",
|
||||
"dockerregistry.lossless.digital": "serve.zone/dcrouter"
|
||||
},
|
||||
"platforms": ["linux/amd64", "linux/arm64"]
|
||||
}
|
||||
}
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
40
Dockerfile
40
Dockerfile
@@ -1,44 +1,24 @@
|
||||
# gitzone dockerfile_service
|
||||
## STAGE 1 // BUILD
|
||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1
|
||||
FROM code.foss.global/host.today/ht-docker-node:lts AS build
|
||||
COPY ./ /app
|
||||
WORKDIR /app
|
||||
ARG NPMCI_TOKEN_NPM2
|
||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
||||
RUN npmci npm prepare
|
||||
RUN pnpm config set store-dir .pnpm-store
|
||||
RUN rm -rf node_modules && pnpm install
|
||||
RUN pnpm run build
|
||||
RUN rm -rf .pnpm-store node_modules && pnpm install --prod
|
||||
|
||||
## STAGE 2 // PRODUCTION
|
||||
FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production
|
||||
|
||||
# gcompat + libstdc++ for glibc-linked Rust binaries (smartproxy, smartmta, remoteingress)
|
||||
RUN apk add --no-cache gcompat libstdc++
|
||||
|
||||
# gitzone dockerfile_service
|
||||
## STAGE 2 // install production
|
||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2
|
||||
WORKDIR /app
|
||||
COPY --from=node1 /app /app
|
||||
RUN rm -rf .pnpm-store
|
||||
ARG NPMCI_TOKEN_NPM2
|
||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
||||
RUN npmci npm prepare
|
||||
RUN pnpm config set store-dir .pnpm-store
|
||||
RUN rm -rf node_modules/ && pnpm install --prod
|
||||
COPY --from=build /app /app
|
||||
|
||||
ENV DCROUTER_MODE=OCI_CONTAINER
|
||||
|
||||
## STAGE 3 // rebuild dependencies for alpine
|
||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3
|
||||
WORKDIR /app
|
||||
COPY --from=node2 /app /app
|
||||
ARG NPMCI_TOKEN_NPM2
|
||||
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
|
||||
RUN npmci npm prepare
|
||||
RUN pnpm config set store-dir .pnpm-store
|
||||
RUN pnpm rebuild -r
|
||||
|
||||
## STAGE 4 // the final production image with all dependencies in place
|
||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4
|
||||
WORKDIR /app
|
||||
COPY --from=node3 /app /app
|
||||
|
||||
### Healthchecks
|
||||
RUN pnpm install -g @servezone/healthy
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
|
||||
|
||||
|
||||
571
changelog.md
571
changelog.md
@@ -1,5 +1,576 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-26 - 11.10.6 - fix(typescript)
|
||||
tighten TypeScript null safety and error handling across backend and ops UI
|
||||
|
||||
- add explicit unknown error typing and safe message access in logging and handler code
|
||||
- mark deferred-initialized class properties with definite assignment assertions to satisfy stricter TypeScript checks
|
||||
- harden ops web state access and action return types with non-null assertions and explicit Promise state typing
|
||||
- update storage reads to allow missing values and align license file references with the lowercase license filename
|
||||
|
||||
## 2026-03-26 - 11.10.5 - fix(build)
|
||||
rename smart tooling config to .smartconfig.json and update package references
|
||||
|
||||
- Moves the shared tool configuration from npmextra.json to .smartconfig.json.
|
||||
- Updates package.json published files and documentation to reference the new config file.
|
||||
- Refreshes several development and runtime dependency versions alongside the config migration.
|
||||
|
||||
## 2026-03-24 - 11.10.4 - fix(monitoring)
|
||||
handle multiple protocol cache entries per backend in metrics output
|
||||
|
||||
- Group detected protocol cache entries by backend host and port so multiple domain-specific records are preserved.
|
||||
- Emit one backend metrics row per cached domain and avoid dropping unmatched protocol cache entries by tracking seen entries with a composite host:port:domain key.
|
||||
- Use cached protocol values when available while keeping backend-only rows for metrics without protocol cache data.
|
||||
|
||||
## 2026-03-23 - 11.10.3 - fix(deps)
|
||||
bump tstest, smartmetrics, and taskbuffer to latest patch releases
|
||||
|
||||
- update @git.zone/tstest from ^3.5.0 to ^3.5.1
|
||||
- update @push.rocks/smartmetrics from ^3.0.2 to ^3.0.3
|
||||
- update @push.rocks/taskbuffer from ^8.0.0 to ^8.0.2
|
||||
|
||||
## 2026-03-23 - 11.10.2 - fix(deps)
|
||||
bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1
|
||||
|
||||
- Updates @api.global/typedserver from ^8.4.2 to ^8.4.6
|
||||
- Updates @push.rocks/smartproxy from ^26.2.0 to ^26.2.1
|
||||
|
||||
## 2026-03-23 - 11.10.1 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^26.2.0
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^26.1.0 to ^26.2.0 in package.json.
|
||||
|
||||
## 2026-03-23 - 11.10.0 - feat(monitoring)
|
||||
add backend protocol metrics to network stats and ops dashboard
|
||||
|
||||
- Expose backend protocol, connection, error, and suppression metrics in stats responses.
|
||||
- Add typed backend info interfaces and app state support for backend metrics.
|
||||
- Render a new backend protocols table in the ops network view with detail modal and suppression badges.
|
||||
- Update smartproxy and lik dependencies to support backend protocol metrics collection.
|
||||
|
||||
## 2026-03-21 - 11.9.1 - fix(lifecycle)
|
||||
clean up service subscriptions, proxy retries, and stale runtime state on shutdown
|
||||
|
||||
- unsubscribe from ServiceManager event streams and use one-time signal handlers to avoid duplicate shutdown execution
|
||||
- reset existing SmartProxy instances before retry setup and prune expired certificate backoff cache entries
|
||||
- add periodic sweeping and shutdown cleanup for stale RADIUS accounting sessions
|
||||
|
||||
## 2026-03-20 - 11.9.0 - feat(dcrouter)
|
||||
add service manager lifecycle orchestration and health-based ops status reporting
|
||||
|
||||
- register dcrouter components with a taskbuffer ServiceManager using dependencies, retries, and critical/optional service roles
|
||||
- update ops stats health output to reflect aggregated service manager state and per-service error or retry details
|
||||
- add @push.rocks/taskbuffer to shared plugins and project dependencies for service lifecycle management
|
||||
|
||||
## 2026-03-20 - 11.8.11 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.10
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.17.9 to ^25.17.10 in package.json
|
||||
|
||||
## 2026-03-20 - 11.8.10 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.9
|
||||
|
||||
- Updates @push.rocks/smartproxy from ^25.17.8 to ^25.17.9 in package.json
|
||||
|
||||
## 2026-03-20 - 11.8.9 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.8
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.17.7 to ^25.17.8.
|
||||
|
||||
## 2026-03-20 - 11.8.8 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.7
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.17.4 to ^25.17.7 in package.json.
|
||||
|
||||
## 2026-03-20 - 11.8.7 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.4
|
||||
|
||||
- updates @push.rocks/smartproxy from ^25.17.3 to ^25.17.4 in package.json
|
||||
|
||||
## 2026-03-20 - 11.8.6 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.3
|
||||
|
||||
- updates @push.rocks/smartproxy from ^25.17.1 to ^25.17.3 in package.json
|
||||
|
||||
## 2026-03-20 - 11.8.5 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.17.1
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.17.0 to ^25.17.1.
|
||||
|
||||
## 2026-03-20 - 11.8.4 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.14.0
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.13.2 to ^4.14.0 in package.json.
|
||||
|
||||
## 2026-03-20 - 11.8.3 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.13.2
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.13.1 to ^4.13.2.
|
||||
|
||||
## 2026-03-19 - 11.8.2 - fix(deps)
|
||||
bump smartproxy and remoteingress dependencies
|
||||
|
||||
- updates @push.rocks/smartproxy from ^25.16.3 to ^25.17.0
|
||||
- updates @serve.zone/remoteingress from ^4.13.0 to ^4.13.1
|
||||
|
||||
## 2026-03-19 - 11.8.1 - fix(dcrouter)
|
||||
use constructor routes for remote ingress setup and bump smartproxy dependency
|
||||
|
||||
- Switch remote ingress initialization to use constructorRoutes instead of smartProxyConfig routes so derived edge ports are based on the active route set.
|
||||
- Update @push.rocks/smartproxy from ^25.16.2 to ^25.16.3.
|
||||
|
||||
## 2026-03-19 - 11.8.0 - feat(remoteingress)
|
||||
add UDP listen port derivation and edge configuration support
|
||||
|
||||
- derive UDP ports from remote ingress routes using transport 'udp' or 'all'
|
||||
- expose effective UDP listen ports in allowed edge payloads and remote ingress interfaces
|
||||
- update @push.rocks/smartproxy to ^25.16.2
|
||||
|
||||
## 2026-03-19 - 11.7.1 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.16.0
|
||||
|
||||
- updates the smartproxy dependency from ^25.15.0 to ^25.16.0
|
||||
|
||||
## 2026-03-19 - 11.7.0 - feat(readme)
|
||||
document HTTP/3 QUIC support and configuration options
|
||||
|
||||
- Add a dedicated README section explaining default HTTP/3 route augmentation, qualification rules, and opt-out behavior.
|
||||
- Document the new global `http3` configuration shape and re-exported `IHttp3Config` type.
|
||||
- Update TypeScript module documentation to include the built-in HTTP/3 augmentation module and exports.
|
||||
|
||||
## 2026-03-19 - 11.6.0 - feat(http3)
|
||||
add automatic HTTP/3 route augmentation for qualifying HTTPS routes
|
||||
|
||||
- introduce configurable HTTP/3 augmentation utilities for eligible SmartProxy routes on port 443
|
||||
- apply HTTP/3 settings to both constructor-defined and stored programmatic routes, with global and per-route opt-out support
|
||||
- export the HTTP/3 config type and add test coverage for qualification, augmentation behavior, and defaults
|
||||
- bump @push.rocks/smartproxy to ^25.15.0 for HTTP/3-related support
|
||||
|
||||
## 2026-03-19 - 11.5.1 - fix(project)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-19 - 11.5.0 - feat(opsserver)
|
||||
add configurable OpsServer port and update related tests and documentation
|
||||
|
||||
- introduces an optional `opsServerPort` configuration that overrides the default OpsServer port 3000
|
||||
- updates OpsServer startup logic to use the configured port
|
||||
- adjusts integration tests to run against dedicated OpsServer ports to avoid conflicts
|
||||
- documents the new OpsServer port option in the README and TypeScript docs
|
||||
- includes dependency updates and a remote ingress port range type refinement
|
||||
|
||||
## 2026-03-19 - 11.4.0 - feat(docs)
|
||||
document OCI container deployment and enable verbose docker build scripts
|
||||
|
||||
- adds a new README section covering Docker/OCI container deployment, environment variables, and image build/push commands
|
||||
- updates docker build and release npm scripts to pass the --verbose flag for more detailed output
|
||||
|
||||
## 2026-03-18 - 11.3.0 - feat(docker)
|
||||
add OCI container startup configuration and migrate Docker release pipeline to tsdocker
|
||||
|
||||
- adds OCI container mode startup that reads DcRouter options from environment variables and an optional JSON config file
|
||||
- simplifies the Docker image to a two-stage build with production dependencies only and Alpine runtime compatibility packages
|
||||
- updates Gitea workflows and npm scripts to use tsdocker for image build and release
|
||||
|
||||
## 2026-03-18 - 11.2.56 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.9.0
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.18 to ^4.9.0 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.55 - fix(deps)
|
||||
bump @serve.zone/catalog to ^2.7.0 and @serve.zone/remoteingress to ^4.8.18
|
||||
|
||||
- updates @serve.zone/catalog from ^2.6.2 to ^2.7.0
|
||||
- updates @serve.zone/remoteingress from ^4.8.16 to ^4.8.18
|
||||
|
||||
## 2026-03-17 - 11.2.54 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.16
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.14 to ^4.8.16 in package.json.
|
||||
|
||||
## 2026-03-17 - 11.2.53 - fix(deps)
|
||||
bump @push.rocks/smartproxy and @serve.zone/remoteingress patch versions
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.23 to ^25.11.24
|
||||
- update @serve.zone/remoteingress from ^4.8.13 to ^4.8.14
|
||||
|
||||
## 2026-03-17 - 11.2.52 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.13
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.8.12 to ^4.8.13.
|
||||
|
||||
## 2026-03-17 - 11.2.51 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.12
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.11 to ^4.8.12 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.50 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.11
|
||||
|
||||
- updates @serve.zone/remoteingress from ^4.8.10 to ^4.8.11
|
||||
|
||||
## 2026-03-17 - 11.2.49 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.10
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.9 to ^4.8.10 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.48 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.9
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.7 to ^4.8.9 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.47 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.23
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.22 to ^25.11.23 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.46 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.22
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.21 to ^25.11.22 in package.json.
|
||||
|
||||
## 2026-03-17 - 11.2.45 - fix(deps)
|
||||
bump @push.rocks/smartproxy and @serve.zone/remoteingress dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.20 to ^25.11.21
|
||||
- update @serve.zone/remoteingress from ^4.8.3 to ^4.8.7
|
||||
|
||||
## 2026-03-17 - 11.2.44 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.3
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.2 to ^4.8.3 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.43 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.2
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.8.1 to ^4.8.2.
|
||||
|
||||
## 2026-03-17 - 11.2.42 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.1
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.8.0 to ^4.8.1 in package.json
|
||||
|
||||
## 2026-03-17 - 11.2.41 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.20
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.19 to ^25.11.20 in package.json.
|
||||
|
||||
## 2026-03-17 - 11.2.40 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.8.0
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.7.2 to ^4.8.0 in package.json.
|
||||
|
||||
## 2026-03-17 - 11.2.39 - fix(repository)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-17 - 11.2.38 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.7.2
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.7.0 to ^4.7.2 in package.json
|
||||
|
||||
## 2026-03-16 - 11.2.37 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.7.0
|
||||
|
||||
- updates the @serve.zone/remoteingress dependency from ^4.6.0 to ^4.7.0
|
||||
|
||||
## 2026-03-16 - 11.2.36 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.19
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.18 to ^25.11.19 in package.json
|
||||
|
||||
## 2026-03-16 - 11.2.35 - fix(deps)
|
||||
bump @push.rocks/smartproxy, @serve.zone/catalog, and @serve.zone/remoteingress dependencies
|
||||
|
||||
- updates @push.rocks/smartproxy from ^25.11.17 to ^25.11.18
|
||||
- updates @serve.zone/catalog from ^2.6.1 to ^2.6.2
|
||||
- updates @serve.zone/remoteingress from ^4.5.11 to ^4.6.0
|
||||
|
||||
## 2026-03-16 - 11.2.34 - fix(deps)
|
||||
bump @push.rocks/smartproxy and @serve.zone/catalog patch versions
|
||||
|
||||
- updates @push.rocks/smartproxy from ^25.11.16 to ^25.11.17
|
||||
- updates @serve.zone/catalog from ^2.6.0 to ^2.6.1
|
||||
|
||||
## 2026-03-16 - 11.2.33 - fix(deps)
|
||||
bump smartproxy and remoteingress dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.14 to ^25.11.16
|
||||
- update @serve.zone/remoteingress from ^4.5.10 to ^4.5.11
|
||||
|
||||
## 2026-03-16 - 11.2.32 - fix(deps)
|
||||
bump smartproxy and remoteingress dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.11 to ^25.11.14
|
||||
- update @serve.zone/remoteingress from ^4.5.9 to ^4.5.10
|
||||
|
||||
## 2026-03-16 - 11.2.31 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.11
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.10 to ^25.11.11 in package.json.
|
||||
|
||||
## 2026-03-16 - 11.2.30 - fix(deps)
|
||||
bump @push.rocks/smartproxy and @serve.zone/catalog dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.9 to ^25.11.10
|
||||
- update @serve.zone/catalog from ^2.5.0 to ^2.6.0
|
||||
|
||||
## 2026-03-16 - 11.2.29 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.9
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.5.8 to ^4.5.9 in package.json
|
||||
|
||||
## 2026-03-16 - 11.2.28 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.8
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.5.7 to ^4.5.8.
|
||||
|
||||
## 2026-03-16 - 11.2.27 - fix(deps)
|
||||
bump smartproxy and remoteingress dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.8 to ^25.11.9
|
||||
- update @serve.zone/remoteingress from ^4.5.5 to ^4.5.7
|
||||
|
||||
## 2026-03-16 - 11.2.26 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.5
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.5.4 to ^4.5.5.
|
||||
|
||||
## 2026-03-16 - 11.2.25 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.8
|
||||
|
||||
- updates the smartproxy dependency from ^25.11.7 to ^25.11.8
|
||||
|
||||
## 2026-03-16 - 11.2.24 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.7
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.6 to ^25.11.7.
|
||||
|
||||
## 2026-03-16 - 11.2.23 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.6
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.11.5 to ^25.11.6.
|
||||
|
||||
## 2026-03-16 - 11.2.22 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.5
|
||||
|
||||
- updates the @push.rocks/smartproxy dependency from ^25.11.4 to ^25.11.5
|
||||
|
||||
## 2026-03-15 - 11.2.21 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.11.4
|
||||
|
||||
- updates the @push.rocks/smartproxy dependency from ^25.11.3 to ^25.11.4
|
||||
|
||||
## 2026-03-15 - 11.2.20 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.4
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.5.3 to ^4.5.4 in package.json
|
||||
|
||||
## 2026-03-15 - 11.2.19 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.3
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.5.2 to ^4.5.3.
|
||||
|
||||
## 2026-03-15 - 11.2.18 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.2
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.5.1 to ^4.5.2 in package.json
|
||||
|
||||
## 2026-03-15 - 11.2.17 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-15 - 11.2.16 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.1
|
||||
|
||||
- Updates @serve.zone/remoteingress from ^4.5.0 to ^4.5.1 in package dependencies.
|
||||
|
||||
## 2026-03-15 - 11.2.15 - fix(deps)
|
||||
bump @serve.zone/remoteingress to ^4.5.0
|
||||
|
||||
- Updates the @serve.zone/remoteingress dependency from ^4.4.1 to ^4.5.0.
|
||||
|
||||
## 2026-03-15 - 11.2.14 - fix(deps)
|
||||
bump smartproxy and remoteingress patch dependencies
|
||||
|
||||
- update @push.rocks/smartproxy from ^25.11.1 to ^25.11.3
|
||||
- update @serve.zone/remoteingress from ^4.4.0 to ^4.4.1
|
||||
|
||||
## 2026-03-15 - 11.2.13 - fix(deps)
|
||||
bump runtime dependencies to latest compatible patch and minor versions
|
||||
|
||||
- update @design.estate/dees-catalog from ^3.48.2 to ^3.48.5
|
||||
- update @push.rocks/smartproxy from ^25.10.7 to ^25.11.1
|
||||
- update @tsclass/tsclass from ^9.3.0 to ^9.4.0
|
||||
- update lru-cache from ^11.2.6 to ^11.2.7
|
||||
|
||||
## 2026-03-12 - 11.2.12 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.10.7
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.10.6 to ^25.10.7 in package.json.
|
||||
|
||||
## 2026-03-12 - 11.2.11 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.10.6
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.10.5 to ^25.10.6 in package.json
|
||||
|
||||
## 2026-03-12 - 11.2.10 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.10.5
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.10.4 to ^25.10.5 in package.json.
|
||||
|
||||
## 2026-03-12 - 11.2.9 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.10.4
|
||||
|
||||
- Updates the @push.rocks/smartproxy dependency from ^25.10.3 to ^25.10.4.
|
||||
|
||||
## 2026-03-12 - 11.2.8 - fix(deps)
|
||||
bump @design.estate/dees-element and @push.rocks/smartproxy patch versions
|
||||
|
||||
- update @design.estate/dees-element from ^2.2.2 to ^2.2.3
|
||||
- update @push.rocks/smartproxy from ^25.10.2 to ^25.10.3
|
||||
|
||||
## 2026-03-12 - 11.2.7 - fix(deps)
|
||||
bump @design.estate/dees-catalog and @push.rocks/smartproxy patch versions
|
||||
|
||||
- update @design.estate/dees-catalog from ^3.48.1 to ^3.48.2
|
||||
- update @push.rocks/smartproxy from ^25.10.1 to ^25.10.2
|
||||
|
||||
## 2026-03-12 - 11.2.6 - fix(deps)
|
||||
bump @design.estate/dees-catalog and @push.rocks/smartproxy patch versions
|
||||
|
||||
- update @design.estate/dees-catalog from ^3.48.0 to ^3.48.1
|
||||
- update @push.rocks/smartproxy from ^25.10.0 to ^25.10.1
|
||||
|
||||
## 2026-03-12 - 11.2.5 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 11.2.4 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 11.2.3 - fix(deps)
|
||||
bump package dependencies to latest compatible patch and minor releases
|
||||
|
||||
- update @types/node to ^25.5.0
|
||||
- upgrade @design.estate/dees-catalog to ^3.48.0 and @design.estate/dees-element to ^2.2.2
|
||||
- bump @push.rocks/smartproxy to ^25.10.0
|
||||
|
||||
## 2026-03-11 - 11.2.2 - fix(deps)
|
||||
update dependencies and devDependencies to newer patch/minor versions
|
||||
|
||||
- devDependency @git.zone/tstest: ^3.3.0 -> ^3.3.2
|
||||
- devDependency @git.zone/tswatch: ^3.2.5 -> ^3.3.0
|
||||
- devDependency @types/node: ^25.3.5 -> ^25.4.0
|
||||
- dependency @design.estate/dees-catalog: ^3.43.3 -> ^3.47.0
|
||||
- dependency @design.estate/dees-element: ^2.1.6 -> ^2.2.1
|
||||
- dependency @push.rocks/smartproxy: ^25.9.2 -> ^25.9.3
|
||||
|
||||
## 2026-03-08 - 11.2.1 - fix(deps)
|
||||
bump devDependency @git.zone/tstest to ^3.3.0 and dependency @push.rocks/smartproxy to ^25.9.2
|
||||
|
||||
- Bumped devDependency @git.zone/tstest: ^3.2.0 -> ^3.3.0
|
||||
- Bumped dependency @push.rocks/smartproxy: ^25.9.1 -> ^25.9.2
|
||||
- Current package version: 11.2.0 — recommended patch release to 11.2.1
|
||||
|
||||
## 2026-03-06 - 11.2.0 - feat(apiclient)
|
||||
add typed, object-oriented API client documentation and interfaces; document builders, resource managers, and new programmatic endpoints
|
||||
|
||||
- Add new @serve.zone/dcrouter-apiclient documentation (ts_apiclient/readme.md) and publish ordering (ts_apiclient/tspublish.json).
|
||||
- Document OO resource classes, fluent builders, auth modes, examples, and API surface for routes, certificates, apiTokens, remoteIngress, stats, config, logs, emails, and radius.
|
||||
- Update main readme: add API Client section, list new client methods, add package entry for @serve.zone/dcrouter-apiclient, and add apiclient test coverage entry.
|
||||
- Update interfaces readme: add Route Management and API Token Management request interfaces and email method changes (getAllEmails, getEmailDetail).
|
||||
- API reference changes: consolidate email endpoints (getAllEmails/getEmailDetail), add route and api token management methods, rename getLogs to getRecentLogs and add getLogStream.
|
||||
- Update web docs to include route & API token management pages and ops view (ops-view-routes)
|
||||
|
||||
## 2026-03-06 - 11.1.0 - feat(apiclient)
|
||||
add TypeScript API client (ts_apiclient) with resource managers and package exports
|
||||
|
||||
- Add new ts_apiclient module providing DcRouterApiClient and resource managers: routes, certificates, api tokens, remote ingress, emails, stats, config, logs, and radius (with sub-managers).
|
||||
- Add resource classes and builders (Route, RemoteIngress, ApiToken, Certificate, Email) and convenience manager APIs for common operations.
|
||||
- Export apiclient in package.json (exports and files) and add ts_apiclient index and plugins wrapper for @api.global/typedrequest.
|
||||
- Add comprehensive tests for the API client (test/test.apiclient.ts).
|
||||
- Bump devDependencies: @git.zone/tsbuild -> ^4.3.0 and @types/node -> ^25.3.5
|
||||
|
||||
## 2026-03-05 - 11.0.51 - fix(build)
|
||||
include HTML files in tsbundle output and bump tsbuild/tsbundle devDependencies
|
||||
|
||||
- Add includeFiles: ["./html/**/*.html"] to bundler config in npmextra.json so HTML assets are included in the bundle
|
||||
- Bump devDependencies: @git.zone/tsbuild ^4.2.4 -> ^4.2.6, @git.zone/tsbundle ^2.9.0 -> ^2.9.1 (non-breaking tooling updates)
|
||||
|
||||
## 2026-03-05 - 11.0.50 - fix(devDependencies)
|
||||
bump @git.zone/tsbuild to ^4.2.4
|
||||
|
||||
- updated devDependency @git.zone/tsbuild from ^4.2.3 to ^4.2.4
|
||||
- no other package changes
|
||||
|
||||
## 2026-03-05 - 11.0.49 - fix(dcrouter)
|
||||
no changes detected
|
||||
|
||||
- No files changed in this commit
|
||||
- Working tree unchanged; no version bump required
|
||||
|
||||
## 2026-03-05 - 11.0.48 - fix(deps)
|
||||
bump @git.zone/tsbuild to ^4.2.3
|
||||
|
||||
- package.json: updated devDependency @git.zone/tsbuild from ^4.2.2 to ^4.2.3
|
||||
|
||||
## 2026-03-05 - 11.0.47 - fix(dcrouter)
|
||||
no code changes; nothing to release
|
||||
|
||||
- No files changed in this commit (git diff is empty)
|
||||
- No version bump required
|
||||
|
||||
## 2026-03-05 - 11.0.46 - fix(none)
|
||||
no changes detected
|
||||
|
||||
- Git diff reported no changes
|
||||
- No files were modified; no version bump required
|
||||
|
||||
## 2026-03-05 - 11.0.45 - fix(deps)
|
||||
bump @git.zone/tsbuild to ^4.2.2
|
||||
|
||||
- Updated @git.zone/tsbuild from ^4.2.1 to ^4.2.2 in package.json
|
||||
|
||||
## 2026-03-05 - 11.0.44 - fix(dev-deps)
|
||||
bump @git.zone/tsbuild devDependency to ^4.2.1
|
||||
|
||||
- Updated package.json devDependency @git.zone/tsbuild from ^4.2.0 to ^4.2.1
|
||||
- Non-breaking patch update for build tool dependency
|
||||
|
||||
## 2026-03-05 - 11.0.43 - fix(dcrouter)
|
||||
no changes detected; nothing to release
|
||||
|
||||
- Git diff reported no changes
|
||||
- No files were modified, so no version bump is recommended
|
||||
|
||||
## 2026-03-05 - 11.0.42 - fix(dcrouter)
|
||||
empty commit — no changes
|
||||
|
||||
- No files were modified in this commit
|
||||
- No version bump required
|
||||
|
||||
## 2026-03-05 - 11.0.41 - fix(deps)
|
||||
bump devDependency @git.zone/tsbuild to ^4.2.0
|
||||
|
||||
- Updated @git.zone/tsbuild from ^4.1.26 to ^4.2.0
|
||||
- Change made in package.json under devDependencies
|
||||
- No source code changes — dev tooling dependency bump
|
||||
|
||||
## 2026-03-05 - 11.0.40 - fix(deps)
|
||||
bump @git.zone/tsbuild devDependency to ^4.1.26
|
||||
|
||||
- Updated devDependency @git.zone/tsbuild: ^4.1.25 → ^4.1.26 in package.json
|
||||
- Build tooling/dev dependency bump only; no runtime or API changes
|
||||
|
||||
## 2026-03-05 - 11.0.39 - fix(devDependencies)
|
||||
bump @git.zone/tsbuild devDependency to ^4.1.25
|
||||
|
||||
- Updated devDependency @git.zone/tsbuild from ^4.1.24 to ^4.1.25 in package.json
|
||||
- Only a devDependency was changed; no runtime dependencies or source files modified
|
||||
- Current package version is 11.0.38; recommend a patch release
|
||||
|
||||
## 2026-03-05 - 11.0.38 - fix(deps)
|
||||
bump @git.zone/tsbuild to ^4.1.24
|
||||
|
||||
|
||||
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
50
package.json
50
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "11.0.38",
|
||||
"version": "11.10.6",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./interfaces": "./dist_ts_interfaces/index.js"
|
||||
"./interfaces": "./dist_ts_interfaces/index.js",
|
||||
"./apiclient": "./dist_ts_apiclient/index.js"
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
@@ -15,52 +16,55 @@
|
||||
"start": "(node --max_old_space_size=250 ./cli.js)",
|
||||
"startTs": "(node cli.ts.js)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||
"build:docker": "tsdocker build --verbose",
|
||||
"release:docker": "tsdocker push --verbose",
|
||||
"bundle": "(tsbundle)",
|
||||
"watch": "tswatch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.24",
|
||||
"@git.zone/tsbundle": "^2.9.0",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.2.0",
|
||||
"@git.zone/tswatch": "^3.2.5",
|
||||
"@types/node": "^25.3.3"
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.0",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.3.0",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^8.4.2",
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.43.3",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@push.rocks/lik": "^6.3.1",
|
||||
"@design.estate/dees-catalog": "^3.49.0",
|
||||
"@design.estate/dees-element": "^2.2.3",
|
||||
"@push.rocks/lik": "^6.4.0",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^9.1.3",
|
||||
"@push.rocks/smartdata": "^7.1.0",
|
||||
"@push.rocks/smartacme": "^9.3.0",
|
||||
"@push.rocks/smartdata": "^7.1.2",
|
||||
"@push.rocks/smartdns": "^7.9.0",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartguard": "^3.1.0",
|
||||
"@push.rocks/smartjwt": "^2.2.1",
|
||||
"@push.rocks/smartlog": "^3.2.1",
|
||||
"@push.rocks/smartmetrics": "^3.0.2",
|
||||
"@push.rocks/smartmetrics": "^3.0.3",
|
||||
"@push.rocks/smartmongo": "^5.1.0",
|
||||
"@push.rocks/smartmta": "^5.3.1",
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^25.9.1",
|
||||
"@push.rocks/smartproxy": "^26.2.4",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.2.0",
|
||||
"@push.rocks/smartstate": "^2.2.1",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@serve.zone/catalog": "^2.5.0",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.9.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@serve.zone/remoteingress": "^4.4.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"lru-cache": "^11.2.6",
|
||||
"@serve.zone/remoteingress": "^4.14.2",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
"lru-cache": "^11.2.7",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -100,13 +104,15 @@
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"ts_apiclient/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"dist_ts_apiclient/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
]
|
||||
}
|
||||
|
||||
3815
pnpm-lock.yaml
generated
3815
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ The project now uses tswatch for development:
|
||||
```bash
|
||||
pnpm run watch
|
||||
```
|
||||
Configuration in `npmextra.json`:
|
||||
Configuration in `.smartconfig.json`:
|
||||
```json
|
||||
{
|
||||
"@git.zone/tswatch": {
|
||||
|
||||
291
readme.md
291
readme.md
@@ -18,6 +18,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [Architecture](#architecture)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [HTTP/HTTPS & TCP/SNI Routing](#httphttps--tcpsni-routing)
|
||||
- [HTTP/3 (QUIC) Support](#http3-quic-support)
|
||||
- [Email System](#email-system)
|
||||
- [DNS Server](#dns-server)
|
||||
- [RADIUS Server](#radius-server)
|
||||
@@ -26,15 +27,18 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [Storage & Caching](#storage--caching)
|
||||
- [Security Features](#security-features)
|
||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||
- [API Client](#api-client)
|
||||
- [API Reference](#api-reference)
|
||||
- [Sub-Modules](#sub-modules)
|
||||
- [Testing](#testing)
|
||||
- [Docker / OCI Container Deployment](#docker--oci-container-deployment)
|
||||
- [License and Legal Information](#license-and-legal-information)
|
||||
|
||||
## Features
|
||||
|
||||
### 🌐 Universal Traffic Router
|
||||
- **HTTP/HTTPS routing** with domain matching, path-based forwarding, and automatic TLS
|
||||
- **HTTP/3 (QUIC) enabled by default** — qualifying HTTPS routes automatically get QUIC/H3 support with zero configuration
|
||||
- **TCP/SNI proxy** for any protocol with TLS termination or passthrough
|
||||
- **DNS server** (Rust-powered via [SmartDNS](https://code.foss.global/push.rocks/smartdns)) with authoritative zones, dynamic record management, and DNS-over-HTTPS
|
||||
- **Multi-protocol support** on the same infrastructure via [SmartProxy](https://code.foss.global/push.rocks/smartproxy)
|
||||
@@ -90,6 +94,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Remote ingress management** with connection token generation and one-click copy
|
||||
- **Read-only configuration display** — DcRouter is configured through code
|
||||
|
||||
### 🔧 Programmatic API Client
|
||||
- **Object-oriented API** — resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) with instance methods
|
||||
- **Builder pattern** — fluent `.setName().setMatch().save()` chains for creating routes, tokens, and edges
|
||||
- **Auto-injected auth** — JWT identity and API tokens included automatically in every request
|
||||
- **Dual auth modes** — login with credentials (JWT) or pass an API token for programmatic access
|
||||
- **Full coverage** — wraps every OpsServer endpoint with typed request/response pairs
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -335,7 +346,7 @@ graph TB
|
||||
|
||||
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
||||
|
||||
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, and RemoteIngress based on which configs are provided.
|
||||
1. **On `start()`**: DcRouter initializes OpsServer (default port 3000, configurable via `opsServerPort`), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, and RemoteIngress based on which configs are provided.
|
||||
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. RemoteIngress runs a Rust data plane for edge tunnel networking. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
||||
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
||||
|
||||
@@ -416,6 +427,31 @@ interface IDcRouterOptions {
|
||||
};
|
||||
};
|
||||
|
||||
// ── HTTP/3 (QUIC) ────────────────────────────────────────────
|
||||
/** HTTP/3 config — enabled by default on qualifying HTTPS routes */
|
||||
http3?: {
|
||||
enabled?: boolean; // default: true
|
||||
quicSettings?: {
|
||||
maxIdleTimeout?: number; // default: 30000ms
|
||||
maxConcurrentBidiStreams?: number; // default: 100
|
||||
maxConcurrentUniStreams?: number; // default: 100
|
||||
initialCongestionWindow?: number;
|
||||
};
|
||||
altSvc?: {
|
||||
port?: number; // default: listening port
|
||||
maxAge?: number; // default: 86400s
|
||||
};
|
||||
udpSettings?: {
|
||||
sessionTimeout?: number; // default: 60000ms
|
||||
maxSessionsPerIP?: number; // default: 1000
|
||||
maxDatagramSize?: number; // default: 65535
|
||||
};
|
||||
};
|
||||
|
||||
// ── OpsServer ────────────────────────────────────────────────
|
||||
/** Port for the OpsServer web dashboard (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
|
||||
// ── TLS & Certificates ────────────────────────────────────────
|
||||
tls?: {
|
||||
contactEmail: string;
|
||||
@@ -503,6 +539,102 @@ DcRouter uses [SmartProxy](https://code.foss.global/push.rocks/smartproxy) for a
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP/3 (QUIC) Support
|
||||
|
||||
DcRouter ships with **HTTP/3 enabled by default** 🚀. All qualifying HTTPS routes on port 443 are automatically augmented with QUIC/H3 configuration — no extra setup needed. Under the hood, SmartProxy's native HTTP/3 support (via `IRouteQuic`) handles QUIC transport, Alt-Svc advertisement, and HTTP/3 negotiation.
|
||||
|
||||
### How It Works
|
||||
|
||||
When DcRouter assembles routes in `setupSmartProxy()`, it automatically augments qualifying routes with:
|
||||
- `match.transport: 'all'` — listen on both TCP (HTTP/1.1 + HTTP/2) and UDP (QUIC/HTTP/3) on the same port
|
||||
- `action.udp.quic` — QUIC configuration with `enableHttp3: true` and `altSvcMaxAge: 86400`
|
||||
|
||||
Browsers that support HTTP/3 will discover it via the `Alt-Svc` header on initial TCP responses, then upgrade to QUIC for subsequent requests.
|
||||
|
||||
### What Gets Augmented
|
||||
|
||||
A route qualifies for HTTP/3 augmentation when **all** of these are true:
|
||||
- Port includes **443** (single number, array, or range)
|
||||
- Action type is **`forward`** (not `socket-handler`)
|
||||
- **TLS is enabled** (passthrough, terminate, or terminate-and-reencrypt)
|
||||
- Route is **not** an email route (ports 25/587/465)
|
||||
- Route doesn't already have `transport: 'all'` or existing `udp.quic` config
|
||||
|
||||
### Zero-Config (Default Behavior)
|
||||
|
||||
```typescript
|
||||
// HTTP/3 is ON by default — this route automatically gets QUIC/H3:
|
||||
const router = new DcRouter({
|
||||
smartProxyConfig: {
|
||||
routes: [{
|
||||
name: 'web-app',
|
||||
match: { domains: ['example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Per-Route Opt-Out
|
||||
|
||||
Disable HTTP/3 on a specific route using `action.options.http3`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'legacy-app',
|
||||
match: { domains: ['legacy.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '192.168.1.50', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: false } // ← This route stays TCP-only
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Opt-Out
|
||||
|
||||
Disable HTTP/3 across all routes:
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
http3: { enabled: false },
|
||||
smartProxyConfig: { routes: [/* ... */] }
|
||||
});
|
||||
```
|
||||
|
||||
### Custom QUIC Settings
|
||||
|
||||
Fine-tune QUIC parameters globally:
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
http3: {
|
||||
quicSettings: {
|
||||
maxIdleTimeout: 60000, // 60s idle timeout
|
||||
maxConcurrentBidiStreams: 200, // More parallel streams
|
||||
maxConcurrentUniStreams: 50,
|
||||
},
|
||||
altSvc: {
|
||||
maxAge: 3600, // 1 hour Alt-Svc cache
|
||||
},
|
||||
udpSettings: {
|
||||
sessionTimeout: 120000, // 2 min UDP session timeout
|
||||
maxSessionsPerIP: 500,
|
||||
}
|
||||
},
|
||||
smartProxyConfig: { routes: [/* ... */] }
|
||||
});
|
||||
```
|
||||
|
||||
### Programmatic Routes
|
||||
|
||||
Routes added at runtime via the Route Management API also get HTTP/3 augmentation automatically — the `RouteConfigManager` applies the same augmentation logic when merging programmatic routes.
|
||||
|
||||
## Email System
|
||||
|
||||
The email system is powered by [`@push.rocks/smartmta`](https://code.foss.global/push.rocks/smartmta), a TypeScript + Rust hybrid MTA. DcRouter configures and orchestrates smartmta's **UnifiedEmailServer**, which handles SMTP sessions, route matching, delivery queuing, DKIM signing, and all email processing.
|
||||
@@ -1007,7 +1139,7 @@ action: {
|
||||
|
||||
## OpsServer Dashboard
|
||||
|
||||
The OpsServer provides a web-based management interface served on port 3000. It's built with modern web components using [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
|
||||
The OpsServer provides a web-based management interface served on port 3000 by default (configurable via `opsServerPort`). It's built with modern web components using [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
|
||||
|
||||
### Dashboard Views
|
||||
|
||||
@@ -1038,12 +1170,9 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'getCombinedMetrics' // All metrics in one call
|
||||
|
||||
// Email Operations
|
||||
'getQueuedEmails' // Emails pending delivery
|
||||
'getSentEmails' // Successfully delivered emails
|
||||
'getFailedEmails' // Failed emails
|
||||
'getAllEmails' // List all emails (queued/sent/failed)
|
||||
'getEmailDetail' // Full detail for a specific email
|
||||
'resendEmail' // Re-queue a failed email
|
||||
'getBounceRecords' // Bounce records
|
||||
'removeFromSuppressionList' // Unsuppress an address
|
||||
|
||||
// Certificates
|
||||
'getCertificateOverview' // Domain-centric certificate status
|
||||
@@ -1062,11 +1191,28 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'getRemoteIngressStatus' // Runtime status of all edges
|
||||
'getRemoteIngressConnectionToken' // Generate a connection token for an edge
|
||||
|
||||
// Route Management (JWT or API token auth)
|
||||
'getMergedRoutes' // List all routes (hardcoded + programmatic)
|
||||
'createRoute' // Create a new programmatic route
|
||||
'updateRoute' // Update a programmatic route
|
||||
'deleteRoute' // Delete a programmatic route
|
||||
'toggleRoute' // Enable/disable a programmatic route
|
||||
'setRouteOverride' // Override a hardcoded route
|
||||
'removeRouteOverride' // Remove a hardcoded route override
|
||||
|
||||
// API Token Management (admin JWT only)
|
||||
'createApiToken' // Create API token → returns raw value once
|
||||
'listApiTokens' // List all tokens (without secrets)
|
||||
'revokeApiToken' // Delete an API token
|
||||
'rollApiToken' // Regenerate token secret
|
||||
'toggleApiToken' // Enable/disable a token
|
||||
|
||||
// Configuration (read-only)
|
||||
'getConfiguration' // Current system config
|
||||
|
||||
// Logs
|
||||
'getLogs' // Retrieve system logs
|
||||
'getRecentLogs' // Retrieve system logs with filtering
|
||||
'getLogStream' // Stream live logs
|
||||
|
||||
// RADIUS
|
||||
'getRadiusSessions' // Active RADIUS sessions
|
||||
@@ -1080,6 +1226,77 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'testVlanAssignment' // Test what VLAN a MAC gets
|
||||
```
|
||||
|
||||
## API Client
|
||||
|
||||
DcRouter ships with a typed, object-oriented API client for programmatic management of a running instance. Install it separately or import from the main package:
|
||||
|
||||
```bash
|
||||
pnpm add @serve.zone/dcrouter-apiclient
|
||||
# or import from the main package:
|
||||
# import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
```
|
||||
|
||||
### Quick Example
|
||||
|
||||
```typescript
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
|
||||
await client.login('admin', 'password');
|
||||
|
||||
// OO resource instances with methods
|
||||
const { routes } = await client.routes.list();
|
||||
await routes[0].toggle(false);
|
||||
|
||||
// Builder pattern for creation
|
||||
const newRoute = await client.routes.build()
|
||||
.setName('api-gateway')
|
||||
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
||||
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||
.setTls({ mode: 'terminate', certificate: 'auto' })
|
||||
.save();
|
||||
|
||||
// Manage certificates
|
||||
const { certificates, summary } = await client.certificates.list();
|
||||
await certificates[0].reprovision();
|
||||
|
||||
// Create API tokens with builder
|
||||
const token = await client.apiTokens.build()
|
||||
.setName('ci-token')
|
||||
.setScopes(['routes:read', 'routes:write'])
|
||||
.setExpiresInDays(90)
|
||||
.save();
|
||||
console.log(token.tokenValue); // only available at creation
|
||||
|
||||
// Remote ingress edges
|
||||
const edge = await client.remoteIngress.build()
|
||||
.setName('edge-nyc-01')
|
||||
.setListenPorts([80, 443])
|
||||
.save();
|
||||
const connToken = await edge.getConnectionToken();
|
||||
|
||||
// Read-only managers
|
||||
const health = await client.stats.getHealth();
|
||||
const config = await client.config.get();
|
||||
const { logs } = await client.logs.getRecent({ level: 'error', limit: 50 });
|
||||
```
|
||||
|
||||
### Resource Managers
|
||||
|
||||
| Manager | Operations |
|
||||
|---------|-----------|
|
||||
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
|
||||
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
|
||||
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
|
||||
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
|
||||
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
|
||||
| `client.config` | `get(section?)` |
|
||||
| `client.logs` | `getRecent()`, `getStream()` |
|
||||
| `client.emails` | `list()` → Email: `getDetail()`, `resend()` |
|
||||
| `client.radius` | `.clients`, `.vlans`, `.sessions` sub-managers + `getStatistics()`, `getAccountingSummary()` |
|
||||
|
||||
See the [full API client documentation](./ts_apiclient/readme.md) for detailed usage of every manager, builder, and resource class.
|
||||
|
||||
## API Reference
|
||||
|
||||
### DcRouter Class
|
||||
@@ -1123,7 +1340,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|
||||
|
||||
### Re-exported Types
|
||||
|
||||
DcRouter re-exports key types from smartmta for convenience:
|
||||
DcRouter re-exports key types for convenience:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
@@ -1133,6 +1350,7 @@ import {
|
||||
type IUnifiedEmailServerOptions,
|
||||
type IEmailRoute,
|
||||
type IEmailDomainConfig,
|
||||
type IHttp3Config,
|
||||
} from '@serve.zone/dcrouter';
|
||||
```
|
||||
|
||||
@@ -1144,12 +1362,14 @@ DcRouter is published as a monorepo with separately-installable interface and we
|
||||
|---------|-------------|---------|
|
||||
| [`@serve.zone/dcrouter`](https://www.npmjs.com/package/@serve.zone/dcrouter) | Main package — the full router | `pnpm add @serve.zone/dcrouter` |
|
||||
| [`@serve.zone/dcrouter-interfaces`](https://www.npmjs.com/package/@serve.zone/dcrouter-interfaces) | TypedRequest interfaces for the OpsServer API | `pnpm add @serve.zone/dcrouter-interfaces` |
|
||||
| [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) | OO API client with builder pattern | `pnpm add @serve.zone/dcrouter-apiclient` |
|
||||
| [`@serve.zone/dcrouter-web`](https://www.npmjs.com/package/@serve.zone/dcrouter-web) | Web dashboard components | `pnpm add @serve.zone/dcrouter-web` |
|
||||
|
||||
You can also import interfaces directly from the main package:
|
||||
You can also import directly from the main package:
|
||||
|
||||
```typescript
|
||||
import { data, requests } from '@serve.zone/dcrouter/interfaces';
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
```
|
||||
|
||||
## Testing
|
||||
@@ -1171,20 +1391,65 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
|
||||
| Test File | Area | Tests |
|
||||
|-----------|------|-------|
|
||||
| `test.apiclient.ts` | API client instantiation, builders, resource hydration, exports | 18 |
|
||||
| `test.contentscanner.ts` | Content scanning (spam, phishing, malware, attachments) | 13 |
|
||||
| `test.dcrouter.email.ts` | Email config, domain and route setup | 4 |
|
||||
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
||||
| `test.dns-socket-handler.ts` | DNS socket handler and route generation | 6 |
|
||||
| `test.errors.ts` | Error classes, handler, retry utilities | 5 |
|
||||
| `test.http3-augmentation.ts` | HTTP/3 route augmentation, qualification, opt-in/out, QUIC settings | 20 |
|
||||
| `test.ipreputationchecker.ts` | IP reputation, DNSBL, caching, risk classification | 10 |
|
||||
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 6 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 8 |
|
||||
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
|
||||
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
||||
|
||||
## Docker / OCI Container Deployment
|
||||
|
||||
DcRouter ships with a `Dockerfile` and supports environment-variable-driven configuration for OCI container deployments. When `DCROUTER_MODE=OCI_CONTAINER` is set, DcRouter automatically reads configuration from environment variables (and optionally from a JSON config file).
|
||||
|
||||
### Running with Docker
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e DCROUTER_MODE=OCI_CONTAINER \
|
||||
-e DCROUTER_TLS_EMAIL=admin@example.com \
|
||||
-e DCROUTER_PUBLIC_IP=203.0.113.1 \
|
||||
-e DCROUTER_DNS_NS_DOMAINS=ns1.example.com,ns2.example.com \
|
||||
-e DCROUTER_DNS_SCOPES=example.com \
|
||||
-p 80:80 -p 443:443 -p 25:25 -p 53:53/udp -p 3000:3000 \
|
||||
code.foss.global/serve.zone/dcrouter:latest
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `DCROUTER_MODE` | Set to `OCI_CONTAINER` to enable container mode | `OCI_CONTAINER` |
|
||||
| `DCROUTER_CONFIG_PATH` | Path to a JSON config file (loaded as base, env vars override) | `/config/dcrouter.json` |
|
||||
| `DCROUTER_BASE_DIR` | Override base data directory | `/data/dcrouter` |
|
||||
| `DCROUTER_TLS_EMAIL` | ACME contact email | `admin@example.com` |
|
||||
| `DCROUTER_TLS_DOMAIN` | Primary TLS domain | `example.com` |
|
||||
| `DCROUTER_PUBLIC_IP` | Public IP for DNS records | `203.0.113.1` |
|
||||
| `DCROUTER_PROXY_IPS` | Comma-separated ingress proxy IPs | `198.51.100.1,198.51.100.2` |
|
||||
| `DCROUTER_DNS_NS_DOMAINS` | Comma-separated nameserver domains | `ns1.example.com,ns2.example.com` |
|
||||
| `DCROUTER_DNS_SCOPES` | Comma-separated authoritative domains | `example.com,other.com` |
|
||||
| `DCROUTER_EMAIL_HOSTNAME` | SMTP server hostname | `mail.example.com` |
|
||||
| `DCROUTER_EMAIL_PORTS` | Comma-separated email ports | `25,587,465` |
|
||||
| `DCROUTER_CACHE_ENABLED` | Enable/disable cache database | `true` |
|
||||
|
||||
### Building the Image
|
||||
|
||||
```bash
|
||||
pnpm run build:docker # Build the container image
|
||||
pnpm run release:docker # Push to registry
|
||||
```
|
||||
|
||||
The Docker build supports multi-platform (`linux/amd64`, `linux/arm64`) via [tsdocker](https://code.foss.global/git.zone/tsdocker).
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
@@ -1196,7 +1461,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
376
test/test.apiclient.ts
Normal file
376
test/test.apiclient.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
DcRouterApiClient,
|
||||
Route,
|
||||
RouteBuilder,
|
||||
RouteManager,
|
||||
Certificate,
|
||||
CertificateManager,
|
||||
ApiToken,
|
||||
ApiTokenBuilder,
|
||||
ApiTokenManager,
|
||||
RemoteIngress,
|
||||
RemoteIngressBuilder,
|
||||
RemoteIngressManager,
|
||||
Email,
|
||||
EmailManager,
|
||||
StatsManager,
|
||||
ConfigManager,
|
||||
LogManager,
|
||||
RadiusManager,
|
||||
RadiusClientManager,
|
||||
RadiusVlanManager,
|
||||
RadiusSessionManager,
|
||||
} from '../ts_apiclient/index.js';
|
||||
|
||||
// =============================================================================
|
||||
// Instantiation & Structure
|
||||
// =============================================================================
|
||||
|
||||
tap.test('DcRouterApiClient - should instantiate with baseUrl', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client).toBeTruthy();
|
||||
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||
expect(client.identity).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should strip trailing slashes from baseUrl', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000///' });
|
||||
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should accept optional apiToken', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_test_token',
|
||||
});
|
||||
expect(client.apiToken).toEqual('dcr_test_token');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should have all resource managers', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client.routes).toBeInstanceOf(RouteManager);
|
||||
expect(client.certificates).toBeInstanceOf(CertificateManager);
|
||||
expect(client.apiTokens).toBeInstanceOf(ApiTokenManager);
|
||||
expect(client.remoteIngress).toBeInstanceOf(RemoteIngressManager);
|
||||
expect(client.stats).toBeInstanceOf(StatsManager);
|
||||
expect(client.config).toBeInstanceOf(ConfigManager);
|
||||
expect(client.logs).toBeInstanceOf(LogManager);
|
||||
expect(client.emails).toBeInstanceOf(EmailManager);
|
||||
expect(client.radius).toBeInstanceOf(RadiusManager);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// buildRequestPayload
|
||||
// =============================================================================
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload includes identity when set', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const identity = {
|
||||
jwt: 'test-jwt',
|
||||
userId: 'user1',
|
||||
name: 'Admin',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
client.identity = identity;
|
||||
|
||||
const payload = client.buildRequestPayload({ extra: 'data' });
|
||||
expect(payload.identity).toEqual(identity);
|
||||
expect(payload.extra).toEqual('data');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload includes apiToken when set', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_abc123',
|
||||
});
|
||||
|
||||
const payload = client.buildRequestPayload();
|
||||
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload with both identity and apiToken', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_abc123',
|
||||
});
|
||||
client.identity = {
|
||||
jwt: 'test-jwt',
|
||||
userId: 'user1',
|
||||
name: 'Admin',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
|
||||
const payload = client.buildRequestPayload({ foo: 'bar' });
|
||||
expect(payload.identity).toBeTruthy();
|
||||
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||
expect(payload.foo).toEqual('bar');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Route Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RouteBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.routes.build();
|
||||
expect(builder).toBeInstanceOf(RouteBuilder);
|
||||
|
||||
// Fluent methods return `this` (same reference)
|
||||
const result = builder
|
||||
.setName('test-route')
|
||||
.setMatch({ ports: 443, domains: 'example.com' })
|
||||
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||
.setEnabled(true);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// ApiToken Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('ApiTokenBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.apiTokens.build();
|
||||
expect(builder).toBeInstanceOf(ApiTokenBuilder);
|
||||
|
||||
const result = builder
|
||||
.setName('ci-token')
|
||||
.setScopes(['routes:read', 'routes:write'])
|
||||
.addScope('config:read')
|
||||
.setExpiresInDays(30);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RemoteIngress Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RemoteIngressBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.remoteIngress.build();
|
||||
expect(builder).toBeInstanceOf(RemoteIngressBuilder);
|
||||
|
||||
const result = builder
|
||||
.setName('edge-1')
|
||||
.setListenPorts([80, 443])
|
||||
.setAutoDerivePorts(true)
|
||||
.setTags(['production']);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Route resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Route - should hydrate from IMergedRoute data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const route = new Route(client, {
|
||||
route: {
|
||||
name: 'test-route',
|
||||
match: { ports: 443, domains: 'example.com' },
|
||||
action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }] },
|
||||
},
|
||||
source: 'programmatic',
|
||||
enabled: true,
|
||||
overridden: false,
|
||||
storedRouteId: 'route-123',
|
||||
createdAt: 1000,
|
||||
updatedAt: 2000,
|
||||
});
|
||||
|
||||
expect(route.name).toEqual('test-route');
|
||||
expect(route.source).toEqual('programmatic');
|
||||
expect(route.enabled).toEqual(true);
|
||||
expect(route.overridden).toEqual(false);
|
||||
expect(route.storedRouteId).toEqual('route-123');
|
||||
expect(route.routeConfig.match.ports).toEqual(443);
|
||||
});
|
||||
|
||||
tap.test('Route - should throw on update/delete/toggle for hardcoded routes', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const route = new Route(client, {
|
||||
route: {
|
||||
name: 'hardcoded-route',
|
||||
match: { ports: 80 },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
|
||||
},
|
||||
source: 'hardcoded',
|
||||
enabled: true,
|
||||
overridden: false,
|
||||
// No storedRouteId for hardcoded routes
|
||||
});
|
||||
|
||||
let updateError: Error | undefined;
|
||||
try {
|
||||
await route.update({ name: 'new-name' });
|
||||
} catch (e) {
|
||||
updateError = e as Error;
|
||||
}
|
||||
expect(updateError).toBeTruthy();
|
||||
expect(updateError!.message).toInclude('hardcoded');
|
||||
|
||||
let deleteError: Error | undefined;
|
||||
try {
|
||||
await route.delete();
|
||||
} catch (e) {
|
||||
deleteError = e as Error;
|
||||
}
|
||||
expect(deleteError).toBeTruthy();
|
||||
|
||||
let toggleError: Error | undefined;
|
||||
try {
|
||||
await route.toggle(false);
|
||||
} catch (e) {
|
||||
toggleError = e as Error;
|
||||
}
|
||||
expect(toggleError).toBeTruthy();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Certificate resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Certificate - should hydrate from ICertificateInfo data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const cert = new Certificate(client, {
|
||||
domain: 'example.com',
|
||||
routeNames: ['main-route'],
|
||||
status: 'valid',
|
||||
source: 'acme',
|
||||
tlsMode: 'terminate',
|
||||
expiryDate: '2027-01-01T00:00:00Z',
|
||||
issuer: "Let's Encrypt",
|
||||
canReprovision: true,
|
||||
});
|
||||
|
||||
expect(cert.domain).toEqual('example.com');
|
||||
expect(cert.status).toEqual('valid');
|
||||
expect(cert.source).toEqual('acme');
|
||||
expect(cert.canReprovision).toEqual(true);
|
||||
expect(cert.routeNames.length).toEqual(1);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// ApiToken resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('ApiToken - should hydrate from IApiTokenInfo data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const token = new ApiToken(
|
||||
client,
|
||||
{
|
||||
id: 'token-1',
|
||||
name: 'ci-token',
|
||||
scopes: ['routes:read', 'routes:write'],
|
||||
createdAt: Date.now(),
|
||||
expiresAt: null,
|
||||
lastUsedAt: null,
|
||||
enabled: true,
|
||||
},
|
||||
'dcr_secret_value',
|
||||
);
|
||||
|
||||
expect(token.id).toEqual('token-1');
|
||||
expect(token.name).toEqual('ci-token');
|
||||
expect(token.scopes.length).toEqual(2);
|
||||
expect(token.enabled).toEqual(true);
|
||||
expect(token.tokenValue).toEqual('dcr_secret_value');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RemoteIngress resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RemoteIngress - should hydrate from IRemoteIngress data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const edge = new RemoteIngress(client, {
|
||||
id: 'edge-1',
|
||||
name: 'test-edge',
|
||||
secret: 'secret123',
|
||||
listenPorts: [80, 443],
|
||||
enabled: true,
|
||||
autoDerivePorts: true,
|
||||
tags: ['prod'],
|
||||
createdAt: 1000,
|
||||
updatedAt: 2000,
|
||||
effectiveListenPorts: [80, 443, 8080],
|
||||
manualPorts: [80, 443],
|
||||
derivedPorts: [8080],
|
||||
});
|
||||
|
||||
expect(edge.id).toEqual('edge-1');
|
||||
expect(edge.name).toEqual('test-edge');
|
||||
expect(edge.listenPorts.length).toEqual(2);
|
||||
expect(edge.effectiveListenPorts!.length).toEqual(3);
|
||||
expect(edge.autoDerivePorts).toEqual(true);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Email resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Email - should hydrate from IEmail data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const email = new Email(client, {
|
||||
id: 'email-1',
|
||||
direction: 'inbound',
|
||||
status: 'delivered',
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Test email',
|
||||
timestamp: '2026-03-06T00:00:00Z',
|
||||
messageId: '<msg-1@example.com>',
|
||||
size: '1234',
|
||||
});
|
||||
|
||||
expect(email.id).toEqual('email-1');
|
||||
expect(email.direction).toEqual('inbound');
|
||||
expect(email.status).toEqual('delivered');
|
||||
expect(email.from).toEqual('sender@example.com');
|
||||
expect(email.subject).toEqual('Test email');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RadiusManager structure
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RadiusManager - should have sub-managers', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client.radius.clients).toBeInstanceOf(RadiusClientManager);
|
||||
expect(client.radius.vlans).toBeInstanceOf(RadiusVlanManager);
|
||||
expect(client.radius.sessions).toBeInstanceOf(RadiusSessionManager);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Exports verification
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Exports - all expected classes should be importable', async () => {
|
||||
expect(DcRouterApiClient).toBeTruthy();
|
||||
expect(Route).toBeTruthy();
|
||||
expect(RouteBuilder).toBeTruthy();
|
||||
expect(RouteManager).toBeTruthy();
|
||||
expect(Certificate).toBeTruthy();
|
||||
expect(CertificateManager).toBeTruthy();
|
||||
expect(ApiToken).toBeTruthy();
|
||||
expect(ApiTokenBuilder).toBeTruthy();
|
||||
expect(ApiTokenManager).toBeTruthy();
|
||||
expect(RemoteIngress).toBeTruthy();
|
||||
expect(RemoteIngressBuilder).toBeTruthy();
|
||||
expect(RemoteIngressManager).toBeTruthy();
|
||||
expect(Email).toBeTruthy();
|
||||
expect(EmailManager).toBeTruthy();
|
||||
expect(StatsManager).toBeTruthy();
|
||||
expect(ConfigManager).toBeTruthy();
|
||||
expect(LogManager).toBeTruthy();
|
||||
expect(RadiusManager).toBeTruthy();
|
||||
expect(RadiusClientManager).toBeTruthy();
|
||||
expect(RadiusVlanManager).toBeTruthy();
|
||||
expect(RadiusSessionManager).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -129,6 +129,7 @@ tap.test('DcRouter class - Email config with domains and routes', async () => {
|
||||
tls: {
|
||||
contactEmail: 'test@example.com'
|
||||
},
|
||||
opsServerPort: 3104,
|
||||
cacheConfig: {
|
||||
enabled: false,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ tap.test('should NOT instantiate DNS server when dnsNsDomains is not set', async
|
||||
smartProxyConfig: {
|
||||
routes: []
|
||||
},
|
||||
opsServerPort: 3100,
|
||||
cacheConfig: { enabled: false }
|
||||
});
|
||||
|
||||
|
||||
304
test/test.http3-augmentation.ts
Normal file
304
test/test.http3-augmentation.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
routeQualifiesForHttp3,
|
||||
augmentRouteWithHttp3,
|
||||
augmentRoutesWithHttp3,
|
||||
type IHttp3Config,
|
||||
} from '../ts/http3/index.js';
|
||||
import type * as plugins from '../ts/plugins.js';
|
||||
|
||||
// Helper to create a basic HTTPS forward route on port 443
|
||||
function makeRoute(
|
||||
overrides: Partial<plugins.smartproxy.IRouteConfig> = {},
|
||||
): plugins.smartproxy.IRouteConfig {
|
||||
return {
|
||||
match: { ports: 443, ...overrides.match },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
...overrides.action,
|
||||
},
|
||||
name: overrides.name ?? 'test-https-route',
|
||||
...Object.fromEntries(
|
||||
Object.entries(overrides).filter(([k]) => !['match', 'action', 'name'].includes(k)),
|
||||
),
|
||||
} as plugins.smartproxy.IRouteConfig;
|
||||
}
|
||||
|
||||
const defaultConfig: IHttp3Config = { enabled: true };
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Qualification tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should augment qualifying HTTPS route on port 443', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp).toBeTruthy();
|
||||
expect(result.action.udp!.quic).toBeTruthy();
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400);
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route on non-443 port', async () => {
|
||||
const route = makeRoute({ match: { ports: 8080 } });
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment socket-handler type route', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'socket-handler' as any,
|
||||
socketHandler: (() => {}) as any,
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route without TLS', async () => {
|
||||
const route: plugins.smartproxy.IRouteConfig = {
|
||||
match: { ports: 443 },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
},
|
||||
name: 'no-tls-route',
|
||||
};
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment email routes', async () => {
|
||||
const emailNames = ['smtp-route', 'submission-route', 'smtps-route', 'email-port-2525-route'];
|
||||
for (const name of emailNames) {
|
||||
const route = makeRoute({ name });
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should respect per-route opt-out (options.http3 = false)', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: false },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should respect per-route opt-in when global is disabled', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: { http3: true },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, { enabled: false });
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should NOT double-augment routes with transport: all', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: 443, transport: 'all' as any },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
// Should be the exact same object (no augmentation)
|
||||
expect(result).toEqual(route);
|
||||
});
|
||||
|
||||
tap.test('should NOT double-augment routes with existing udp.quic', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
udp: { quic: { enableHttp3: true } },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result).toEqual(route);
|
||||
});
|
||||
|
||||
tap.test('should augment route with port range including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [{ from: 400, to: 500 }] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should augment route with port array including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [80, 443] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should NOT augment route with port range NOT including 443', async () => {
|
||||
const route = makeRoute({
|
||||
match: { ports: [{ from: 8000, to: 9000 }] },
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should augment TLS passthrough routes', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'passthrough' },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should augment terminate-and-reencrypt routes', async () => {
|
||||
const route = makeRoute({
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'localhost', port: 8080 }],
|
||||
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
|
||||
},
|
||||
});
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Configuration tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should apply default QUIC settings when none provided', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(86400);
|
||||
// Undefined means SmartProxy will use its own defaults
|
||||
expect(result.action.udp!.quic!.maxIdleTimeout).toBeUndefined();
|
||||
expect(result.action.udp!.quic!.altSvcPort).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should apply custom QUIC settings', async () => {
|
||||
const route = makeRoute();
|
||||
const config: IHttp3Config = {
|
||||
enabled: true,
|
||||
quicSettings: {
|
||||
maxIdleTimeout: 60000,
|
||||
maxConcurrentBidiStreams: 200,
|
||||
maxConcurrentUniStreams: 50,
|
||||
initialCongestionWindow: 65536,
|
||||
},
|
||||
altSvc: {
|
||||
port: 8443,
|
||||
maxAge: 3600,
|
||||
},
|
||||
udpSettings: {
|
||||
sessionTimeout: 120000,
|
||||
maxSessionsPerIP: 500,
|
||||
maxDatagramSize: 32768,
|
||||
},
|
||||
};
|
||||
const result = augmentRouteWithHttp3(route, config);
|
||||
|
||||
expect(result.action.udp!.quic!.maxIdleTimeout).toEqual(60000);
|
||||
expect(result.action.udp!.quic!.maxConcurrentBidiStreams).toEqual(200);
|
||||
expect(result.action.udp!.quic!.maxConcurrentUniStreams).toEqual(50);
|
||||
expect(result.action.udp!.quic!.initialCongestionWindow).toEqual(65536);
|
||||
expect(result.action.udp!.quic!.altSvcPort).toEqual(8443);
|
||||
expect(result.action.udp!.quic!.altSvcMaxAge).toEqual(3600);
|
||||
expect(result.action.udp!.sessionTimeout).toEqual(120000);
|
||||
expect(result.action.udp!.maxSessionsPerIP).toEqual(500);
|
||||
expect(result.action.udp!.maxDatagramSize).toEqual(32768);
|
||||
});
|
||||
|
||||
tap.test('should not mutate the original route', async () => {
|
||||
const route = makeRoute();
|
||||
const originalTransport = route.match.transport;
|
||||
const originalUdp = route.action.udp;
|
||||
|
||||
augmentRouteWithHttp3(route, defaultConfig);
|
||||
|
||||
expect(route.match.transport).toEqual(originalTransport);
|
||||
expect(route.action.udp).toEqual(originalUdp);
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Batch augmentation
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should augment multiple routes in a batch', async () => {
|
||||
const routes = [
|
||||
makeRoute({ name: 'web-app' }),
|
||||
makeRoute({ name: 'smtp-route', match: { ports: 25 } }),
|
||||
makeRoute({ name: 'api-gateway' }),
|
||||
makeRoute({
|
||||
name: 'dns-query',
|
||||
action: { type: 'socket-handler' as any, socketHandler: (() => {}) as any },
|
||||
}),
|
||||
];
|
||||
|
||||
const results = augmentRoutesWithHttp3(routes, defaultConfig);
|
||||
|
||||
// web-app and api-gateway should be augmented
|
||||
expect(results[0].match.transport).toEqual('all');
|
||||
expect(results[2].match.transport).toEqual('all');
|
||||
|
||||
// smtp and dns should NOT be augmented
|
||||
expect(results[1].match.transport).toBeUndefined();
|
||||
expect(results[3].match.transport).toBeUndefined();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Default enabled behavior
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
tap.test('should treat undefined enabled as true (default on)', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, {}); // no enabled field at all
|
||||
|
||||
expect(result.match.transport).toEqual('all');
|
||||
expect(result.action.udp!.quic!.enableHttp3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should disable when enabled is explicitly false', async () => {
|
||||
const route = makeRoute();
|
||||
const result = augmentRouteWithHttp3(route, { enabled: false });
|
||||
|
||||
expect(result.match.transport).toBeUndefined();
|
||||
expect(result.action.udp).toBeUndefined();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -9,6 +9,7 @@ let identity: interfaces.data.IIdentity;
|
||||
tap.test('should start DCRouter with OpsServer', async () => {
|
||||
testDcRouter = new DcRouter({
|
||||
// Minimal config for testing
|
||||
opsServerPort: 3102,
|
||||
cacheConfig: { enabled: false },
|
||||
});
|
||||
|
||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
||||
|
||||
tap.test('should login with admin credentials and receive JWT', async () => {
|
||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'adminLoginWithUsernameAndPassword'
|
||||
);
|
||||
|
||||
@@ -41,7 +42,7 @@ tap.test('should login with admin credentials and receive JWT', async () => {
|
||||
|
||||
tap.test('should verify valid JWT identity', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -57,7 +58,7 @@ tap.test('should verify valid JWT identity', async () => {
|
||||
|
||||
tap.test('should reject invalid JWT', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -74,7 +75,7 @@ tap.test('should reject invalid JWT', async () => {
|
||||
|
||||
tap.test('should verify JWT matches identity data', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -91,7 +92,7 @@ tap.test('should verify JWT matches identity data', async () => {
|
||||
|
||||
tap.test('should handle logout', async () => {
|
||||
const logoutRequest = new TypedRequest<interfaces.requests.IReq_AdminLogout>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'adminLogout'
|
||||
);
|
||||
|
||||
@@ -105,7 +106,7 @@ tap.test('should handle logout', async () => {
|
||||
|
||||
tap.test('should reject wrong credentials', async () => {
|
||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3102/typedrequest',
|
||||
'adminLoginWithUsernameAndPassword'
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ let adminIdentity: interfaces.data.IIdentity;
|
||||
tap.test('should start DCRouter with OpsServer', async () => {
|
||||
testDcRouter = new DcRouter({
|
||||
// Minimal config for testing
|
||||
opsServerPort: 3101,
|
||||
cacheConfig: { enabled: false },
|
||||
});
|
||||
|
||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
||||
|
||||
tap.test('should login as admin', async () => {
|
||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'adminLoginWithUsernameAndPassword'
|
||||
);
|
||||
|
||||
@@ -33,7 +34,7 @@ tap.test('should login as admin', async () => {
|
||||
|
||||
tap.test('should respond to health status request', async () => {
|
||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'getHealthStatus'
|
||||
);
|
||||
|
||||
@@ -49,7 +50,7 @@ tap.test('should respond to health status request', async () => {
|
||||
|
||||
tap.test('should respond to server statistics request', async () => {
|
||||
const statsRequest = new TypedRequest<interfaces.requests.IReq_GetServerStatistics>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'getServerStatistics'
|
||||
);
|
||||
|
||||
@@ -66,7 +67,7 @@ tap.test('should respond to server statistics request', async () => {
|
||||
|
||||
tap.test('should respond to configuration request', async () => {
|
||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'getConfiguration'
|
||||
);
|
||||
|
||||
@@ -87,7 +88,7 @@ tap.test('should respond to configuration request', async () => {
|
||||
|
||||
tap.test('should handle log retrieval request', async () => {
|
||||
const logsRequest = new TypedRequest<interfaces.requests.IReq_GetRecentLogs>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'getRecentLogs'
|
||||
);
|
||||
|
||||
@@ -104,7 +105,7 @@ tap.test('should handle log retrieval request', async () => {
|
||||
|
||||
tap.test('should reject unauthenticated requests', async () => {
|
||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3101/typedrequest',
|
||||
'getHealthStatus'
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ let adminIdentity: interfaces.data.IIdentity;
|
||||
tap.test('should start DCRouter with OpsServer', async () => {
|
||||
testDcRouter = new DcRouter({
|
||||
// Minimal config for testing
|
||||
opsServerPort: 3103,
|
||||
cacheConfig: { enabled: false },
|
||||
});
|
||||
|
||||
@@ -18,7 +19,7 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
||||
|
||||
tap.test('should login as admin', async () => {
|
||||
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'adminLoginWithUsernameAndPassword'
|
||||
);
|
||||
|
||||
@@ -34,7 +35,7 @@ tap.test('should login as admin', async () => {
|
||||
|
||||
tap.test('should allow admin to verify identity', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -49,7 +50,7 @@ tap.test('should allow admin to verify identity', async () => {
|
||||
|
||||
tap.test('should reject verify identity without identity', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -64,7 +65,7 @@ tap.test('should reject verify identity without identity', async () => {
|
||||
|
||||
tap.test('should reject verify identity with invalid JWT', async () => {
|
||||
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'verifyIdentity'
|
||||
);
|
||||
|
||||
@@ -84,7 +85,7 @@ tap.test('should reject verify identity with invalid JWT', async () => {
|
||||
|
||||
tap.test('should reject protected endpoints without auth', async () => {
|
||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'getHealthStatus'
|
||||
);
|
||||
|
||||
@@ -100,7 +101,7 @@ tap.test('should reject protected endpoints without auth', async () => {
|
||||
|
||||
tap.test('should allow authenticated access to protected endpoints', async () => {
|
||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'http://localhost:3103/typedrequest',
|
||||
'getConfiguration'
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.0.38',
|
||||
version: '11.10.6',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
20
ts/cache/classes.cache.cleaner.ts
vendored
20
ts/cache/classes.cache.cleaner.ts
vendored
@@ -48,14 +48,14 @@ export class CacheCleaner {
|
||||
this.isRunning = true;
|
||||
|
||||
// Run cleanup immediately on start
|
||||
this.runCleanup().catch((error) => {
|
||||
logger.log('error', `Initial cache cleanup failed: ${error.message}`);
|
||||
this.runCleanup().catch((error: unknown) => {
|
||||
logger.log('error', `Initial cache cleanup failed: ${(error as Error).message}`);
|
||||
});
|
||||
|
||||
// Schedule periodic cleanup
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.runCleanup().catch((error) => {
|
||||
logger.log('error', `Cache cleanup failed: ${error.message}`);
|
||||
this.runCleanup().catch((error: unknown) => {
|
||||
logger.log('error', `Cache cleanup failed: ${(error as Error).message}`);
|
||||
});
|
||||
}, this.options.intervalMs);
|
||||
|
||||
@@ -113,8 +113,8 @@ export class CacheCleaner {
|
||||
`Cache cleanup completed. Deleted ${totalDeleted} expired documents. ${summary || 'No deletions.'}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Cache cleanup error: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Cache cleanup error: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -138,14 +138,14 @@ export class CacheCleaner {
|
||||
try {
|
||||
await doc.delete();
|
||||
deletedCount++;
|
||||
} catch (deleteError) {
|
||||
logger.log('warn', `Failed to delete expired document: ${deleteError.message}`);
|
||||
} catch (deleteError: unknown) {
|
||||
logger.log('warn', `Failed to delete expired document: ${(deleteError as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error cleaning collection: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error cleaning collection: ${(error as Error).message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
2
ts/cache/classes.cached.document.ts
vendored
2
ts/cache/classes.cached.document.ts
vendored
@@ -22,7 +22,7 @@ export abstract class CachedDocument<T extends CachedDocument<T>> extends plugin
|
||||
* Timestamp when the document expires and should be cleaned up
|
||||
* NOTE: Subclasses must add @svDb() decorator
|
||||
*/
|
||||
public expiresAt: Date;
|
||||
public expiresAt!: Date;
|
||||
|
||||
/**
|
||||
* Timestamp of last access (for LRU-style eviction if needed)
|
||||
|
||||
12
ts/cache/classes.cachedb.ts
vendored
12
ts/cache/classes.cachedb.ts
vendored
@@ -23,8 +23,8 @@ export interface ICacheDbOptions {
|
||||
export class CacheDb {
|
||||
private static instance: CacheDb | null = null;
|
||||
|
||||
private localTsmDb: plugins.smartmongo.LocalTsmDb;
|
||||
private smartdataDb: plugins.smartdata.SmartdataDb;
|
||||
private localTsmDb!: plugins.smartmongo.LocalTsmDb;
|
||||
private smartdataDb!: plugins.smartdata.SmartdataDb;
|
||||
private options: Required<ICacheDbOptions>;
|
||||
private isStarted: boolean = false;
|
||||
|
||||
@@ -89,8 +89,8 @@ export class CacheDb {
|
||||
|
||||
this.isStarted = true;
|
||||
logger.log('info', `CacheDb started at ${this.options.storagePath}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to start CacheDb: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to start CacheDb: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -116,8 +116,8 @@ export class CacheDb {
|
||||
|
||||
this.isStarted = false;
|
||||
logger.log('info', 'CacheDb stopped');
|
||||
} catch (error) {
|
||||
logger.log('error', `Error stopping CacheDb: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error stopping CacheDb: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
28
ts/cache/documents/classes.cached.email.ts
vendored
28
ts/cache/documents/classes.cached.email.ts
vendored
@@ -35,55 +35,55 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
|
||||
*/
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
/**
|
||||
* Email message ID (RFC 822 Message-ID header)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public messageId: string;
|
||||
public messageId!: string;
|
||||
|
||||
/**
|
||||
* Sender email address (envelope from)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public from: string;
|
||||
public from!: string;
|
||||
|
||||
/**
|
||||
* Recipient email addresses
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public to: string[];
|
||||
public to!: string[];
|
||||
|
||||
/**
|
||||
* CC recipients
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public cc: string[];
|
||||
public cc!: string[];
|
||||
|
||||
/**
|
||||
* BCC recipients
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public bcc: string[];
|
||||
public bcc!: string[];
|
||||
|
||||
/**
|
||||
* Email subject
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public subject: string;
|
||||
public subject!: string;
|
||||
|
||||
/**
|
||||
* Raw RFC822 email content
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public rawContent: string;
|
||||
public rawContent!: string;
|
||||
|
||||
/**
|
||||
* Current status of the email
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public status: TCachedEmailStatus;
|
||||
public status!: TCachedEmailStatus;
|
||||
|
||||
/**
|
||||
* Number of delivery attempts
|
||||
@@ -101,25 +101,25 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
|
||||
* Timestamp for next delivery attempt
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public nextAttempt: Date;
|
||||
public nextAttempt!: Date;
|
||||
|
||||
/**
|
||||
* Last error message if delivery failed
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public lastError: string;
|
||||
public lastError!: string;
|
||||
|
||||
/**
|
||||
* Timestamp when the email was successfully delivered
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public deliveredAt: Date;
|
||||
public deliveredAt!: Date;
|
||||
|
||||
/**
|
||||
* Sender domain (for querying/filtering)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public senderDomain: string;
|
||||
public senderDomain!: string;
|
||||
|
||||
/**
|
||||
* Priority level (higher = more important)
|
||||
@@ -131,7 +131,7 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
|
||||
* JSON-serialized route data
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public routeData: string;
|
||||
public routeData!: string;
|
||||
|
||||
/**
|
||||
* DKIM signature status
|
||||
|
||||
@@ -45,61 +45,61 @@ export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
|
||||
*/
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public ipAddress: string;
|
||||
public ipAddress!: string;
|
||||
|
||||
/**
|
||||
* Reputation score (0-100, higher = better)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public score: number;
|
||||
public score!: number;
|
||||
|
||||
/**
|
||||
* Whether the IP is flagged as spam source
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isSpam: boolean;
|
||||
public isSpam!: boolean;
|
||||
|
||||
/**
|
||||
* Whether the IP is a known proxy
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isProxy: boolean;
|
||||
public isProxy!: boolean;
|
||||
|
||||
/**
|
||||
* Whether the IP is a Tor exit node
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isTor: boolean;
|
||||
public isTor!: boolean;
|
||||
|
||||
/**
|
||||
* Whether the IP is a VPN endpoint
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public isVPN: boolean;
|
||||
public isVPN!: boolean;
|
||||
|
||||
/**
|
||||
* Country code (ISO 3166-1 alpha-2)
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public country: string;
|
||||
public country!: string;
|
||||
|
||||
/**
|
||||
* Autonomous System Number
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public asn: string;
|
||||
public asn!: string;
|
||||
|
||||
/**
|
||||
* Organization name
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public org: string;
|
||||
public org!: string;
|
||||
|
||||
/**
|
||||
* List of blacklists the IP appears on
|
||||
*/
|
||||
@plugins.smartdata.svDb()
|
||||
public blacklists: string[];
|
||||
public blacklists!: string[];
|
||||
|
||||
/**
|
||||
* Number of times this IP has been checked
|
||||
|
||||
@@ -61,14 +61,21 @@ export class CertProvisionScheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain is currently in backoff
|
||||
* Check if a domain is currently in backoff.
|
||||
* Expired entries are pruned from the cache to prevent unbounded growth.
|
||||
*/
|
||||
async isInBackoff(domain: string): Promise<boolean> {
|
||||
const entry = await this.loadBackoff(domain);
|
||||
if (!entry) return false;
|
||||
|
||||
const retryAfter = new Date(entry.retryAfter);
|
||||
return retryAfter.getTime() > Date.now();
|
||||
if (retryAfter.getTime() > Date.now()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Backoff has expired — prune the stale entry
|
||||
this.backoffCache.delete(domain);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,9 +131,12 @@ export class CertProvisionScheduler {
|
||||
const entry = await this.loadBackoff(domain);
|
||||
if (!entry) return null;
|
||||
|
||||
// Only return if still in backoff
|
||||
// Only return if still in backoff — prune expired entries
|
||||
const retryAfter = new Date(entry.retryAfter);
|
||||
if (retryAfter.getTime() <= Date.now()) return null;
|
||||
if (retryAfter.getTime() <= Date.now()) {
|
||||
this.backoffCache.delete(domain);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
failures: entry.failures,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
||||
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||
@@ -163,6 +164,17 @@ export interface IDcRouterOptions {
|
||||
* Remote Ingress configuration for edge tunnel nodes
|
||||
* Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
|
||||
*/
|
||||
/**
|
||||
* HTTP/3 (QUIC) configuration for HTTPS routes.
|
||||
* Enabled by default — qualifying HTTPS routes on port 443 are automatically
|
||||
* augmented with QUIC/H3 fields. Set { enabled: false } to disable globally.
|
||||
* Individual routes can opt out via action.options.http3 = false.
|
||||
*/
|
||||
http3?: IHttp3Config;
|
||||
|
||||
/** Port for the OpsServer web UI (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
|
||||
remoteIngressConfig?: {
|
||||
/** Enable remote ingress hub (default: false) */
|
||||
enabled?: boolean;
|
||||
@@ -203,7 +215,7 @@ export class DcRouter {
|
||||
public emailServer?: UnifiedEmailServer;
|
||||
public radiusServer?: RadiusServer;
|
||||
public storageManager: StorageManager;
|
||||
public opsServer: OpsServer;
|
||||
public opsServer!: OpsServer;
|
||||
public metricsManager?: MetricsManager;
|
||||
|
||||
// Cache system (smartdata + LocalTsmDb)
|
||||
@@ -240,6 +252,11 @@ export class DcRouter {
|
||||
// Certificate provisioning scheduler with per-domain backoff
|
||||
public certProvisionScheduler?: CertProvisionScheduler;
|
||||
|
||||
// Service lifecycle management
|
||||
public serviceManager: plugins.taskbuffer.ServiceManager;
|
||||
private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
|
||||
public smartAcmeReady = false;
|
||||
|
||||
// TypedRouter for API endpoints
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
@@ -267,66 +284,253 @@ export class DcRouter {
|
||||
|
||||
// Initialize storage manager
|
||||
this.storageManager = new StorageManager(this.options.storage);
|
||||
|
||||
// Initialize service manager and register all services
|
||||
this.serviceManager = new plugins.taskbuffer.ServiceManager({
|
||||
name: 'dcrouter',
|
||||
startupTimeoutMs: 120_000,
|
||||
shutdownTimeoutMs: 30_000,
|
||||
});
|
||||
this.registerServices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all dcrouter services with the ServiceManager.
|
||||
* Services are started in dependency order, with failure isolation for optional services.
|
||||
*/
|
||||
private registerServices(): void {
|
||||
// OpsServer: critical, no dependencies — provides visibility
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('OpsServer')
|
||||
.critical()
|
||||
.withStart(async () => {
|
||||
this.opsServer = new OpsServer(this);
|
||||
await this.opsServer.start();
|
||||
})
|
||||
.withStop(async () => {
|
||||
await this.opsServer?.stop();
|
||||
})
|
||||
.withRetry({ maxRetries: 0 }),
|
||||
);
|
||||
|
||||
// CacheDb: optional, no dependencies
|
||||
if (this.options.cacheConfig?.enabled !== false) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('CacheDb')
|
||||
.optional()
|
||||
.withStart(async () => {
|
||||
await this.setupCacheDb();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.cacheCleaner) {
|
||||
this.cacheCleaner.stop();
|
||||
this.cacheCleaner = undefined;
|
||||
}
|
||||
if (this.cacheDb) {
|
||||
await this.cacheDb.stop();
|
||||
CacheDb.resetInstance();
|
||||
this.cacheDb = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 2, baseDelayMs: 1000, maxDelayMs: 5000 }),
|
||||
);
|
||||
}
|
||||
|
||||
// MetricsManager: optional, depends on OpsServer
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('MetricsManager')
|
||||
.optional()
|
||||
.dependsOn('OpsServer')
|
||||
.withStart(async () => {
|
||||
this.metricsManager = new MetricsManager(this);
|
||||
await this.metricsManager.start();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.metricsManager) {
|
||||
await this.metricsManager.stop();
|
||||
this.metricsManager = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 1, baseDelayMs: 1000 }),
|
||||
);
|
||||
|
||||
// SmartProxy: critical, depends on CacheDb (if enabled)
|
||||
const smartProxyDeps: string[] = [];
|
||||
if (this.options.cacheConfig?.enabled !== false) {
|
||||
smartProxyDeps.push('CacheDb');
|
||||
}
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('SmartProxy')
|
||||
.critical()
|
||||
.dependsOn(...smartProxyDeps)
|
||||
.withStart(async () => {
|
||||
await this.setupSmartProxy();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.smartProxy) {
|
||||
this.smartProxy.removeAllListeners();
|
||||
await this.smartProxy.stop();
|
||||
this.smartProxy = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 0 }),
|
||||
);
|
||||
|
||||
// SmartAcme: optional, depends on SmartProxy — aggressive retry for rate limits
|
||||
// Only registered if DNS challenge is configured
|
||||
if (this.options.dnsChallenge?.cloudflareApiKey) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('SmartAcme')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
if (this.smartAcme) {
|
||||
await this.smartAcme.start();
|
||||
this.smartAcmeReady = true;
|
||||
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
||||
}
|
||||
})
|
||||
.withStop(async () => {
|
||||
this.smartAcmeReady = false;
|
||||
if (this.smartAcme) {
|
||||
await this.smartAcme.stop();
|
||||
this.smartAcme = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 20, baseDelayMs: 5000, maxDelayMs: 3_600_000, backoffFactor: 2 }),
|
||||
);
|
||||
}
|
||||
|
||||
// ConfigManagers: optional, depends on SmartProxy
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('ConfigManagers')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
this.routeConfigManager = new RouteConfigManager(
|
||||
this.storageManager,
|
||||
() => this.getConstructorRoutes(),
|
||||
() => this.smartProxy,
|
||||
() => this.options.http3,
|
||||
);
|
||||
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||
await this.apiTokenManager.initialize();
|
||||
await this.routeConfigManager.initialize();
|
||||
})
|
||||
.withStop(async () => {
|
||||
this.routeConfigManager = undefined;
|
||||
this.apiTokenManager = undefined;
|
||||
})
|
||||
.withRetry({ maxRetries: 2, baseDelayMs: 1000 }),
|
||||
);
|
||||
|
||||
// Email Server: optional, depends on SmartProxy
|
||||
if (this.options.emailConfig) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('EmailServer')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
await this.setupUnifiedEmailHandling();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.emailServer) {
|
||||
if ((this.emailServer as any).deliverySystem) {
|
||||
(this.emailServer as any).deliverySystem.removeAllListeners();
|
||||
}
|
||||
this.emailServer.removeAllListeners();
|
||||
await this.emailServer.stop();
|
||||
this.emailServer = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
||||
);
|
||||
}
|
||||
|
||||
// DNS Server: optional, depends on SmartProxy
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('DnsServer')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
await this.setupDnsWithSocketHandler();
|
||||
})
|
||||
.withStop(async () => {
|
||||
// Flush pending DNS batch log
|
||||
if (this.dnsBatchTimer) {
|
||||
clearTimeout(this.dnsBatchTimer);
|
||||
if (this.dnsBatchCount > 0) {
|
||||
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (final flush)`, { zone: 'dns' });
|
||||
}
|
||||
this.dnsBatchTimer = null;
|
||||
this.dnsBatchCount = 0;
|
||||
this.dnsLogWindowSecond = 0;
|
||||
this.dnsLogWindowCount = 0;
|
||||
}
|
||||
if (this.dnsServer) {
|
||||
this.dnsServer.removeAllListeners();
|
||||
await this.dnsServer.stop();
|
||||
this.dnsServer = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
||||
);
|
||||
}
|
||||
|
||||
// RADIUS Server: optional, no dependency on SmartProxy
|
||||
if (this.options.radiusConfig) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('RadiusServer')
|
||||
.optional()
|
||||
.withStart(async () => {
|
||||
await this.setupRadiusServer();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.radiusServer) {
|
||||
await this.radiusServer.stop();
|
||||
this.radiusServer = undefined;
|
||||
}
|
||||
})
|
||||
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
||||
);
|
||||
}
|
||||
|
||||
// Remote Ingress: optional, depends on SmartProxy
|
||||
if (this.options.remoteIngressConfig?.enabled) {
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('RemoteIngress')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
await this.setupRemoteIngress();
|
||||
})
|
||||
.withStop(async () => {
|
||||
if (this.tunnelManager) {
|
||||
await this.tunnelManager.stop();
|
||||
this.tunnelManager = undefined;
|
||||
}
|
||||
this.remoteIngressManager = undefined;
|
||||
})
|
||||
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
||||
);
|
||||
}
|
||||
|
||||
// Wire up aggregated events for logging
|
||||
this.serviceSubjectSubscription = this.serviceManager.serviceSubject.subscribe((event) => {
|
||||
const level = event.type === 'failed' ? 'error' : event.type === 'retrying' ? 'warn' : 'info';
|
||||
logger.log(level as any, `Service '${event.serviceName}': ${event.type}`, {
|
||||
state: event.state,
|
||||
...(event.error ? { error: event.error } : {}),
|
||||
...(event.attempt ? { attempt: event.attempt } : {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
logger.log('info', 'Starting DcRouter Services');
|
||||
|
||||
|
||||
this.opsServer = new OpsServer(this);
|
||||
await this.opsServer.start();
|
||||
|
||||
try {
|
||||
// Initialize cache database if enabled (default: enabled)
|
||||
if (this.options.cacheConfig?.enabled !== false) {
|
||||
await this.setupCacheDb();
|
||||
}
|
||||
|
||||
// Initialize MetricsManager
|
||||
this.metricsManager = new MetricsManager(this);
|
||||
await this.metricsManager.start();
|
||||
|
||||
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
||||
await this.setupSmartProxy();
|
||||
|
||||
// Initialize programmatic config API managers
|
||||
this.routeConfigManager = new RouteConfigManager(
|
||||
this.storageManager,
|
||||
() => this.getConstructorRoutes(),
|
||||
() => this.smartProxy,
|
||||
);
|
||||
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||
await this.apiTokenManager.initialize();
|
||||
await this.routeConfigManager.initialize();
|
||||
|
||||
// Set up unified email handling if configured
|
||||
if (this.options.emailConfig) {
|
||||
await this.setupUnifiedEmailHandling();
|
||||
}
|
||||
|
||||
// Set up DNS server if configured with nameservers and scopes
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 &&
|
||||
this.options.dnsScopes && this.options.dnsScopes.length > 0) {
|
||||
await this.setupDnsWithSocketHandler();
|
||||
}
|
||||
|
||||
// Set up RADIUS server if configured
|
||||
if (this.options.radiusConfig) {
|
||||
await this.setupRadiusServer();
|
||||
}
|
||||
|
||||
// Set up Remote Ingress hub if configured
|
||||
if (this.options.remoteIngressConfig?.enabled) {
|
||||
await this.setupRemoteIngress();
|
||||
}
|
||||
|
||||
this.logStartupSummary();
|
||||
} catch (error) {
|
||||
logger.log('error', 'Error starting DcRouter', { error: String(error) });
|
||||
// Try to clean up any services that may have started
|
||||
await this.stop();
|
||||
throw error;
|
||||
}
|
||||
await this.serviceManager.start();
|
||||
this.logStartupSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,7 +590,21 @@ export class DcRouter {
|
||||
logger.log('info', `Cache Database: storage=${this.cacheDb.getStoragePath()}, db=${this.cacheDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
||||
}
|
||||
|
||||
logger.log('info', 'All services are running');
|
||||
// Service status summary from ServiceManager
|
||||
const health = this.serviceManager.getHealth();
|
||||
const statuses = health.services;
|
||||
const running = statuses.filter(s => s.state === 'running').length;
|
||||
const failed = statuses.filter(s => s.state === 'failed').length;
|
||||
const retrying = statuses.filter(s => s.state === 'starting' || s.state === 'degraded').length;
|
||||
|
||||
if (failed > 0) {
|
||||
const failedNames = statuses.filter(s => s.state === 'failed').map(s => `${s.name}: ${s.lastError || 'unknown'}`);
|
||||
logger.log('warn', `DcRouter started in degraded mode — ${running} running, ${failed} failed: ${failedNames.join('; ')}`);
|
||||
} else if (retrying > 0) {
|
||||
logger.log('info', `DcRouter started — ${running} running, ${retrying} still initializing`);
|
||||
} else {
|
||||
logger.log('info', `All ${running} services are running`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,6 +640,13 @@ export class DcRouter {
|
||||
*/
|
||||
private async setupSmartProxy(): Promise<void> {
|
||||
logger.log('info', 'Setting up SmartProxy...');
|
||||
|
||||
// Clean up any existing SmartProxy instance (e.g. from a retry)
|
||||
if (this.smartProxy) {
|
||||
this.smartProxy.removeAllListeners();
|
||||
this.smartProxy = undefined;
|
||||
}
|
||||
|
||||
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
||||
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
||||
|
||||
@@ -466,6 +691,13 @@ export class DcRouter {
|
||||
challengeHandlers.push(dns01Handler);
|
||||
}
|
||||
|
||||
// HTTP/3 augmentation (enabled by default unless explicitly disabled)
|
||||
if (this.options.http3?.enabled !== false) {
|
||||
const http3Config: IHttp3Config = { enabled: true, ...this.options.http3 };
|
||||
routes = augmentRoutesWithHttp3(routes, http3Config);
|
||||
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
||||
}
|
||||
|
||||
// Cache constructor routes for RouteConfigManager
|
||||
this.constructorRoutes = [...routes];
|
||||
|
||||
@@ -515,10 +747,13 @@ export class DcRouter {
|
||||
// Initialize cert provision scheduler
|
||||
this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager);
|
||||
|
||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||
// If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction
|
||||
// Note: SmartAcme.start() is NOT called here — it runs as a separate optional service
|
||||
// via the ServiceManager, with aggressive retry for rate-limit resilience.
|
||||
if (challengeHandlers.length > 0) {
|
||||
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
||||
if (this.smartAcme) {
|
||||
this.smartAcmeReady = false;
|
||||
await this.smartAcme.stop().catch(err =>
|
||||
logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
|
||||
);
|
||||
@@ -530,10 +765,15 @@ export class DcRouter {
|
||||
challengeHandlers: challengeHandlers,
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
|
||||
const scheduler = this.certProvisionScheduler;
|
||||
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
||||
// If SmartAcme is not yet ready (still starting or retrying), fall back to HTTP-01
|
||||
if (!this.smartAcmeReady) {
|
||||
eventComms.warn(`SmartAcme not yet initialized, falling back to http-01 for ${domain}`);
|
||||
return 'http01';
|
||||
}
|
||||
|
||||
// Check backoff before attempting provision
|
||||
if (await scheduler.isInBackoff(domain)) {
|
||||
const info = await scheduler.getBackoffInfo(domain);
|
||||
@@ -547,7 +787,7 @@ export class DcRouter {
|
||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||
eventComms.setSource('smartacme-dns-01');
|
||||
const isWildcardDomain = domain.startsWith('*.');
|
||||
const cert = await this.smartAcme.getCertificateForDomain(domain, {
|
||||
const cert = await this.smartAcme!.getCertificateForDomain(domain, {
|
||||
includeWildcard: !isWildcardDomain,
|
||||
});
|
||||
if (cert.validUntil) {
|
||||
@@ -566,10 +806,10 @@ export class DcRouter {
|
||||
// Success — clear any backoff
|
||||
await scheduler.clearBackoff(domain);
|
||||
return result;
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
// Record failure for backoff tracking
|
||||
await scheduler.recordFailure(domain, err.message);
|
||||
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
||||
await scheduler.recordFailure(domain, (err as Error).message);
|
||||
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${(err as Error).message}, falling back to http-01`);
|
||||
return 'http01';
|
||||
}
|
||||
};
|
||||
@@ -894,105 +1134,29 @@ export class DcRouter {
|
||||
public async stop() {
|
||||
logger.log('info', 'Stopping DcRouter services...');
|
||||
|
||||
// Flush pending DNS batch log
|
||||
if (this.dnsBatchTimer) {
|
||||
clearTimeout(this.dnsBatchTimer);
|
||||
if (this.dnsBatchCount > 0) {
|
||||
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (rate limited, final flush)`, { zone: 'dns' });
|
||||
}
|
||||
this.dnsBatchTimer = null;
|
||||
this.dnsBatchCount = 0;
|
||||
this.dnsLogWindowSecond = 0;
|
||||
this.dnsLogWindowCount = 0;
|
||||
// Unsubscribe from service events before stopping services
|
||||
if (this.serviceSubjectSubscription) {
|
||||
this.serviceSubjectSubscription.unsubscribe();
|
||||
this.serviceSubjectSubscription = undefined;
|
||||
}
|
||||
|
||||
await this.opsServer.stop();
|
||||
// ServiceManager handles reverse-dependency-ordered shutdown
|
||||
await this.serviceManager.stop();
|
||||
|
||||
try {
|
||||
// Remove event listeners before stopping services to prevent leaks
|
||||
if (this.smartProxy) {
|
||||
this.smartProxy.removeAllListeners();
|
||||
}
|
||||
if (this.emailServer) {
|
||||
if ((this.emailServer as any).deliverySystem) {
|
||||
(this.emailServer as any).deliverySystem.removeAllListeners();
|
||||
}
|
||||
this.emailServer.removeAllListeners();
|
||||
}
|
||||
if (this.dnsServer) {
|
||||
this.dnsServer.removeAllListeners();
|
||||
}
|
||||
|
||||
// Stop all services in parallel for faster shutdown
|
||||
await Promise.all([
|
||||
// Stop cache cleaner if running
|
||||
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
|
||||
|
||||
// Stop metrics manager if running
|
||||
this.metricsManager ? this.metricsManager.stop().catch(err => logger.log('error', 'Error stopping MetricsManager', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop unified email server if running
|
||||
this.emailServer ? this.emailServer.stop().catch(err => logger.log('error', 'Error stopping email server', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop SmartAcme if running
|
||||
this.smartAcme ? this.smartAcme.stop().catch(err => logger.log('error', 'Error stopping SmartAcme', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop HTTP SmartProxy if running
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => logger.log('error', 'Error stopping SmartProxy', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop DNS server if running
|
||||
this.dnsServer ?
|
||||
this.dnsServer.stop().catch(err => logger.log('error', 'Error stopping DNS server', { error: String(err) })) :
|
||||
Promise.resolve(),
|
||||
|
||||
// Stop RADIUS server if running
|
||||
this.radiusServer ?
|
||||
this.radiusServer.stop().catch(err => logger.log('error', 'Error stopping RADIUS server', { error: String(err) })) :
|
||||
Promise.resolve(),
|
||||
|
||||
// Stop Remote Ingress tunnel manager if running
|
||||
this.tunnelManager ?
|
||||
this.tunnelManager.stop().catch(err => logger.log('error', 'Error stopping TunnelManager', { error: String(err) })) :
|
||||
Promise.resolve()
|
||||
]);
|
||||
|
||||
// Stop cache database after other services (they may need it during shutdown)
|
||||
if (this.cacheDb) {
|
||||
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
|
||||
CacheDb.resetInstance();
|
||||
}
|
||||
|
||||
// Clear backoff cache in cert scheduler
|
||||
if (this.certProvisionScheduler) {
|
||||
this.certProvisionScheduler.clear();
|
||||
}
|
||||
|
||||
// Allow GC of stopped services by nulling references
|
||||
this.smartProxy = undefined;
|
||||
this.emailServer = undefined;
|
||||
this.dnsServer = undefined;
|
||||
this.metricsManager = undefined;
|
||||
this.cacheCleaner = undefined;
|
||||
this.cacheDb = undefined;
|
||||
this.tunnelManager = undefined;
|
||||
this.radiusServer = undefined;
|
||||
this.smartAcme = undefined;
|
||||
// Clear backoff cache in cert scheduler
|
||||
if (this.certProvisionScheduler) {
|
||||
this.certProvisionScheduler.clear();
|
||||
this.certProvisionScheduler = undefined;
|
||||
this.remoteIngressManager = undefined;
|
||||
this.routeConfigManager = undefined;
|
||||
this.apiTokenManager = undefined;
|
||||
this.certificateStatusMap.clear();
|
||||
|
||||
// Reset security singletons to allow GC
|
||||
SecurityLogger.resetInstance();
|
||||
ContentScanner.resetInstance();
|
||||
IPReputationChecker.resetInstance();
|
||||
|
||||
logger.log('info', 'All DcRouter services stopped');
|
||||
} catch (error) {
|
||||
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.certificateStatusMap.clear();
|
||||
|
||||
// Reset security singletons to allow GC
|
||||
SecurityLogger.resetInstance();
|
||||
ContentScanner.resetInstance();
|
||||
IPReputationChecker.resetInstance();
|
||||
|
||||
logger.log('info', 'All DcRouter services stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1084,21 +1248,21 @@ export class DcRouter {
|
||||
// Wire delivery events to MetricsManager and logger
|
||||
if (this.metricsManager && this.emailServer.deliverySystem) {
|
||||
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
|
||||
this.metricsManager.trackEmailReceived(item?.from);
|
||||
this.metricsManager!.trackEmailReceived(item?.from);
|
||||
logger.log('info', `Email delivery started: ${item?.from} → ${item?.to}`, { zone: 'email' });
|
||||
});
|
||||
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
|
||||
this.metricsManager.trackEmailSent(item?.to);
|
||||
this.metricsManager!.trackEmailSent(item?.to);
|
||||
logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
|
||||
});
|
||||
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
|
||||
this.metricsManager.trackEmailFailed(item?.to, error?.message);
|
||||
this.metricsManager!.trackEmailFailed(item?.to, error?.message);
|
||||
logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
|
||||
});
|
||||
}
|
||||
if (this.metricsManager && this.emailServer) {
|
||||
this.emailServer.on('bounceProcessed', () => {
|
||||
this.metricsManager.trackEmailBounced();
|
||||
this.metricsManager!.trackEmailBounced();
|
||||
logger.log('warn', 'Email bounce processed', { zone: 'email' });
|
||||
});
|
||||
}
|
||||
@@ -1141,12 +1305,12 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
logger.log('info', 'All unified email components stopped');
|
||||
} catch (error) {
|
||||
logger.log('error', `Error stopping unified email components: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error stopping unified email components: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update domain rules for email routing
|
||||
* @param rules New domain rules to apply
|
||||
@@ -1304,7 +1468,7 @@ export class DcRouter {
|
||||
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
||||
// Metrics tracking
|
||||
for (const question of event.questions) {
|
||||
this.metricsManager.trackDnsQuery(
|
||||
this.metricsManager?.trackDnsQuery(
|
||||
question.type,
|
||||
question.name,
|
||||
false,
|
||||
@@ -1389,8 +1553,8 @@ export class DcRouter {
|
||||
// Use the built-in socket handler from smartdns
|
||||
// This handles HTTP/2, DoH protocol, etc.
|
||||
await (this.dnsServer as any).handleHttpsSocket(socket);
|
||||
} catch (error) {
|
||||
logger.log('error', `DNS socket handler error: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
@@ -1531,14 +1695,14 @@ export class DcRouter {
|
||||
} else {
|
||||
logger.log('warn', `Invalid DKIM record structure in ${file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to load DKIM record from ${file}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to load DKIM record from ${file}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to load DKIM records: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to load DKIM records: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -1570,11 +1734,11 @@ export class DcRouter {
|
||||
// This ensures keys are ready even if DNS mode changes later
|
||||
await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain);
|
||||
logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.log('info', 'DKIM initialization complete');
|
||||
}
|
||||
|
||||
@@ -1615,10 +1779,10 @@ export class DcRouter {
|
||||
} else {
|
||||
logger.log('warn', 'Could not auto-discover public IPv4 address');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to auto-discover public IP: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
|
||||
if (!publicIp) {
|
||||
logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
|
||||
}
|
||||
@@ -1712,8 +1876,8 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to detect public IP: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1733,7 +1897,7 @@ export class DcRouter {
|
||||
await this.remoteIngressManager.initialize();
|
||||
|
||||
// Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
|
||||
const currentRoutes = this.options.smartProxyConfig?.routes || [];
|
||||
const currentRoutes = this.constructorRoutes;
|
||||
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
|
||||
|
||||
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
||||
@@ -1747,8 +1911,8 @@ export class DcRouter {
|
||||
const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
|
||||
tlsConfig = { certPem, keyPem };
|
||||
logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
|
||||
} catch (err) {
|
||||
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
IMergedRoute,
|
||||
IRouteWarning,
|
||||
} from '../../ts_interfaces/data/route-management.js';
|
||||
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
||||
|
||||
const ROUTES_PREFIX = '/config-api/routes/';
|
||||
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
||||
@@ -20,6 +21,7 @@ export class RouteConfigManager {
|
||||
private storageManager: StorageManager,
|
||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -258,10 +260,15 @@ export class RouteConfigManager {
|
||||
enabledRoutes.push(route);
|
||||
}
|
||||
|
||||
// Add enabled programmatic routes
|
||||
// Add enabled programmatic routes (with HTTP/3 augmentation if enabled)
|
||||
const http3Config = this.getHttp3Config?.();
|
||||
for (const stored of this.storedRoutes.values()) {
|
||||
if (stored.enabled) {
|
||||
enabledRoutes.push(stored.route);
|
||||
if (http3Config && http3Config.enabled !== false) {
|
||||
enabledRoutes.push(augmentRouteWithHttp3(stored.route, { enabled: true, ...http3Config }));
|
||||
} else {
|
||||
enabledRoutes.push(stored.route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export class ConfigValidator {
|
||||
} else if (rules.items.schema && itemType === 'object') {
|
||||
const itemResult = this.validate(value[i], rules.items.schema);
|
||||
if (!itemResult.valid) {
|
||||
errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
|
||||
errors.push(...itemResult.errors!.map(err => `${key}[${i}].${err}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@ export class ConfigValidator {
|
||||
if (rules.schema) {
|
||||
const nestedResult = this.validate(value, rules.schema);
|
||||
if (!nestedResult.valid) {
|
||||
errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
|
||||
errors.push(...nestedResult.errors!.map(err => `${key}.${err}`));
|
||||
}
|
||||
validatedConfig[key] = nestedResult.config;
|
||||
}
|
||||
@@ -233,8 +233,8 @@ export class ConfigValidator {
|
||||
|
||||
// Apply defaults to array items
|
||||
if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
|
||||
result[key] = result[key].map(item =>
|
||||
typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
|
||||
result[key] = result[key].map(item =>
|
||||
typeof item === 'object' ? this.applyDefaults(item, rules.items!.schema!) : item
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export class ConfigValidator {
|
||||
|
||||
if (!result.valid) {
|
||||
throw new ValidationError(
|
||||
`Configuration validation failed: ${result.errors.join(', ')}`,
|
||||
`Configuration validation failed: ${result.errors!.join(', ')}`,
|
||||
'CONFIG_VALIDATION_ERROR',
|
||||
{ data: { errors: result.errors } }
|
||||
);
|
||||
|
||||
@@ -227,7 +227,7 @@ export class PlatformError extends Error {
|
||||
const { retry } = this.context;
|
||||
if (!retry) return false;
|
||||
|
||||
return retry.currentRetry < retry.maxRetries;
|
||||
return (retry.currentRetry ?? 0) < (retry.maxRetries ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
153
ts/http3/http3-route-augmentation.ts
Normal file
153
ts/http3/http3-route-augmentation.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Configuration for HTTP/3 (QUIC) route augmentation.
|
||||
* HTTP/3 is enabled by default on all qualifying HTTPS routes.
|
||||
*/
|
||||
export interface IHttp3Config {
|
||||
/** Enable HTTP/3 augmentation on qualifying routes (default: true) */
|
||||
enabled?: boolean;
|
||||
/** QUIC-specific settings applied to all augmented routes */
|
||||
quicSettings?: {
|
||||
/** QUIC connection idle timeout in ms (default: 30000) */
|
||||
maxIdleTimeout?: number;
|
||||
/** Max concurrent bidirectional streams per connection (default: 100) */
|
||||
maxConcurrentBidiStreams?: number;
|
||||
/** Max concurrent unidirectional streams per connection (default: 100) */
|
||||
maxConcurrentUniStreams?: number;
|
||||
/** Initial congestion window size in bytes */
|
||||
initialCongestionWindow?: number;
|
||||
};
|
||||
/** Alt-Svc header settings */
|
||||
altSvc?: {
|
||||
/** Port advertised in Alt-Svc header (default: same as listening port) */
|
||||
port?: number;
|
||||
/** Max age for Alt-Svc advertisement in seconds (default: 86400) */
|
||||
maxAge?: number;
|
||||
};
|
||||
/** UDP session settings */
|
||||
udpSettings?: {
|
||||
/** Idle timeout for UDP sessions in ms (default: 60000) */
|
||||
sessionTimeout?: number;
|
||||
/** Max concurrent UDP sessions per source IP (default: 1000) */
|
||||
maxSessionsPerIP?: number;
|
||||
/** Max accepted datagram size in bytes (default: 65535) */
|
||||
maxDatagramSize?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type TPortRange = plugins.smartproxy.IRouteConfig['match']['ports'];
|
||||
|
||||
/**
|
||||
* Check whether a TPortRange includes port 443.
|
||||
*/
|
||||
function portRangeIncludes443(ports: TPortRange): boolean {
|
||||
if (typeof ports === 'number') return ports === 443;
|
||||
if (Array.isArray(ports)) {
|
||||
return ports.some((p) => {
|
||||
if (typeof p === 'number') return p === 443;
|
||||
return p.from <= 443 && p.to >= 443;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route name indicates an email route that should not get HTTP/3.
|
||||
*/
|
||||
function isEmailRoute(route: plugins.smartproxy.IRouteConfig): boolean {
|
||||
const name = route.name?.toLowerCase() || '';
|
||||
return (
|
||||
name.startsWith('smtp-') ||
|
||||
name.startsWith('submission-') ||
|
||||
name.startsWith('smtps-') ||
|
||||
name.startsWith('email-')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a route qualifies for HTTP/3 augmentation.
|
||||
*/
|
||||
export function routeQualifiesForHttp3(
|
||||
route: plugins.smartproxy.IRouteConfig,
|
||||
globalConfig: IHttp3Config,
|
||||
): boolean {
|
||||
// Check global enable + per-route override
|
||||
const globalEnabled = globalConfig.enabled !== false; // default true
|
||||
const perRouteOverride = route.action.options?.http3;
|
||||
|
||||
// If per-route explicitly set, use that; otherwise use global
|
||||
const shouldAugment =
|
||||
perRouteOverride !== undefined ? perRouteOverride : globalEnabled;
|
||||
if (!shouldAugment) return false;
|
||||
|
||||
// Must be forward type
|
||||
if (route.action.type !== 'forward') return false;
|
||||
|
||||
// Must include port 443
|
||||
if (!portRangeIncludes443(route.match.ports)) return false;
|
||||
|
||||
// Must have TLS
|
||||
if (!route.action.tls) return false;
|
||||
|
||||
// Skip email routes
|
||||
if (isEmailRoute(route)) return false;
|
||||
|
||||
// Skip if already configured with transport 'all' or 'udp'
|
||||
if (route.match.transport === 'all' || route.match.transport === 'udp') return false;
|
||||
|
||||
// Skip if already has QUIC config
|
||||
if (route.action.udp?.quic) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment a single route with HTTP/3 fields.
|
||||
* Returns a new route object (does not mutate the original).
|
||||
*/
|
||||
export function augmentRouteWithHttp3(
|
||||
route: plugins.smartproxy.IRouteConfig,
|
||||
config: IHttp3Config,
|
||||
): plugins.smartproxy.IRouteConfig {
|
||||
if (!routeQualifiesForHttp3(route, config)) {
|
||||
return route;
|
||||
}
|
||||
|
||||
return {
|
||||
...route,
|
||||
match: {
|
||||
...route.match,
|
||||
transport: 'all' as const,
|
||||
},
|
||||
action: {
|
||||
...route.action,
|
||||
udp: {
|
||||
...(route.action.udp || {}),
|
||||
sessionTimeout: config.udpSettings?.sessionTimeout,
|
||||
maxSessionsPerIP: config.udpSettings?.maxSessionsPerIP,
|
||||
maxDatagramSize: config.udpSettings?.maxDatagramSize,
|
||||
quic: {
|
||||
enableHttp3: true,
|
||||
maxIdleTimeout: config.quicSettings?.maxIdleTimeout,
|
||||
maxConcurrentBidiStreams: config.quicSettings?.maxConcurrentBidiStreams,
|
||||
maxConcurrentUniStreams: config.quicSettings?.maxConcurrentUniStreams,
|
||||
altSvcPort: config.altSvc?.port,
|
||||
altSvcMaxAge: config.altSvc?.maxAge ?? 86400,
|
||||
initialCongestionWindow: config.quicSettings?.initialCongestionWindow,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment all qualifying routes in an array.
|
||||
* Returns a new array (does not mutate originals).
|
||||
*/
|
||||
export function augmentRoutesWithHttp3(
|
||||
routes: plugins.smartproxy.IRouteConfig[],
|
||||
config: IHttp3Config,
|
||||
): plugins.smartproxy.IRouteConfig[] {
|
||||
return routes.map((route) => augmentRouteWithHttp3(route, config));
|
||||
}
|
||||
1
ts/http3/index.ts
Normal file
1
ts/http3/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './http3-route-augmentation.js';
|
||||
26
ts/index.ts
26
ts/index.ts
@@ -5,6 +5,7 @@ export { UnifiedEmailServer } from '@push.rocks/smartmta';
|
||||
export type { IUnifiedEmailServerOptions, IEmailRoute, IEmailDomainConfig } from '@push.rocks/smartmta';
|
||||
|
||||
// DcRouter
|
||||
import { DcRouter } from './classes.dcrouter.js';
|
||||
export * from './classes.dcrouter.js';
|
||||
|
||||
// RADIUS module
|
||||
@@ -13,4 +14,27 @@ export * from './radius/index.js';
|
||||
// Remote Ingress module
|
||||
export * from './remoteingress/index.js';
|
||||
|
||||
export const runCli = async () => {};
|
||||
// HTTP/3 module
|
||||
export type { IHttp3Config } from './http3/index.js';
|
||||
|
||||
export const runCli = async () => {
|
||||
let options: import('./classes.dcrouter.js').IDcRouterOptions = {};
|
||||
|
||||
if (process.env.DCROUTER_MODE === 'OCI_CONTAINER') {
|
||||
const { getOciContainerConfig } = await import('../ts_oci_container/index.js');
|
||||
options = getOciContainerConfig();
|
||||
console.log('[DCRouter] Starting in OCI Container mode...');
|
||||
}
|
||||
|
||||
const dcRouter = new DcRouter(options);
|
||||
await dcRouter.start();
|
||||
console.log('[DCRouter] Running. Send SIGTERM or SIGINT to stop.');
|
||||
|
||||
const shutdown = async () => {
|
||||
console.log('[DCRouter] Shutting down...');
|
||||
await dcRouter.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
process.once('SIGINT', shutdown);
|
||||
process.once('SIGTERM', shutdown);
|
||||
};
|
||||
|
||||
@@ -296,11 +296,11 @@ export class MetricsManager {
|
||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||
|
||||
if (!proxyMetrics) {
|
||||
return [];
|
||||
return [] as Array<{ type: string; count: number; source: string; lastActivity: Date }>;
|
||||
}
|
||||
|
||||
|
||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||
const connectionInfo = [];
|
||||
const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
|
||||
|
||||
for (const [routeName, count] of connectionsByRoute) {
|
||||
connectionInfo.push({
|
||||
@@ -558,6 +558,7 @@ export class MetricsManager {
|
||||
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
backends: [] as Array<any>,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -590,6 +591,110 @@ export class MetricsManager {
|
||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||
const requestsTotal = proxyMetrics.requests.total();
|
||||
|
||||
// Collect backend protocol data
|
||||
const backendMetrics = proxyMetrics.backends.byBackend();
|
||||
const protocolCache = proxyMetrics.backends.detectedProtocols();
|
||||
|
||||
// Group protocol cache entries by host:port so we can match them to backend metrics.
|
||||
// The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
|
||||
// can have multiple entries for different domains.
|
||||
const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
|
||||
for (const entry of protocolCache) {
|
||||
const backendKey = `${entry.host}:${entry.port}`;
|
||||
let entries = cacheByBackend.get(backendKey);
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
cacheByBackend.set(backendKey, entries);
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
const backends: Array<any> = [];
|
||||
const seenCacheKeys = new Set<string>();
|
||||
|
||||
for (const [key, bm] of backendMetrics) {
|
||||
const cacheEntries = cacheByBackend.get(key);
|
||||
if (!cacheEntries || cacheEntries.length === 0) {
|
||||
// No protocol cache entry — emit one row with backend metrics only
|
||||
backends.push({
|
||||
backend: key,
|
||||
domain: null,
|
||||
protocol: bm.protocol,
|
||||
activeConnections: bm.activeConnections,
|
||||
totalConnections: bm.totalConnections,
|
||||
connectErrors: bm.connectErrors,
|
||||
handshakeErrors: bm.handshakeErrors,
|
||||
requestErrors: bm.requestErrors,
|
||||
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
||||
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
||||
h2Failures: bm.h2Failures,
|
||||
h2Suppressed: false,
|
||||
h3Suppressed: false,
|
||||
h2CooldownRemainingSecs: null,
|
||||
h3CooldownRemainingSecs: null,
|
||||
h2ConsecutiveFailures: null,
|
||||
h3ConsecutiveFailures: null,
|
||||
h3Port: null,
|
||||
cacheAgeSecs: null,
|
||||
});
|
||||
} else {
|
||||
// One row per domain, each enriched with the shared backend metrics
|
||||
for (const cache of cacheEntries) {
|
||||
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
|
||||
seenCacheKeys.add(compositeKey);
|
||||
backends.push({
|
||||
backend: key,
|
||||
domain: cache.domain ?? null,
|
||||
protocol: cache.protocol ?? bm.protocol,
|
||||
activeConnections: bm.activeConnections,
|
||||
totalConnections: bm.totalConnections,
|
||||
connectErrors: bm.connectErrors,
|
||||
handshakeErrors: bm.handshakeErrors,
|
||||
requestErrors: bm.requestErrors,
|
||||
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
||||
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
||||
h2Failures: bm.h2Failures,
|
||||
h2Suppressed: cache.h2Suppressed,
|
||||
h3Suppressed: cache.h3Suppressed,
|
||||
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
|
||||
h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
|
||||
h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
|
||||
h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
|
||||
h3Port: cache.h3Port,
|
||||
cacheAgeSecs: cache.ageSecs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include protocol cache entries with no matching backend metric
|
||||
for (const entry of protocolCache) {
|
||||
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
|
||||
if (!seenCacheKeys.has(compositeKey)) {
|
||||
backends.push({
|
||||
backend: `${entry.host}:${entry.port}`,
|
||||
domain: entry.domain,
|
||||
protocol: entry.protocol,
|
||||
activeConnections: 0,
|
||||
totalConnections: 0,
|
||||
connectErrors: 0,
|
||||
handshakeErrors: 0,
|
||||
requestErrors: 0,
|
||||
avgConnectTimeMs: 0,
|
||||
poolHitRate: 0,
|
||||
h2Failures: 0,
|
||||
h2Suppressed: entry.h2Suppressed,
|
||||
h3Suppressed: entry.h3Suppressed,
|
||||
h2CooldownRemainingSecs: entry.h2CooldownRemainingSecs,
|
||||
h3CooldownRemainingSecs: entry.h3CooldownRemainingSecs,
|
||||
h2ConsecutiveFailures: entry.h2ConsecutiveFailures,
|
||||
h3ConsecutiveFailures: entry.h3ConsecutiveFailures,
|
||||
h3Port: entry.h3Port,
|
||||
cacheAgeSecs: entry.ageSecs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connectionsByIP,
|
||||
throughputRate,
|
||||
@@ -599,6 +704,7 @@ export class MetricsManager {
|
||||
throughputByIP,
|
||||
requestsPerSecond,
|
||||
requestsTotal,
|
||||
backends,
|
||||
};
|
||||
}, 1000); // 1s cache — matches typical dashboard poll interval
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js'
|
||||
|
||||
export class OpsServer {
|
||||
public dcRouterRef: DcRouter;
|
||||
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
||||
public server!: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
||||
|
||||
// Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
@@ -17,17 +17,17 @@ export class OpsServer {
|
||||
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
|
||||
|
||||
// Handler instances
|
||||
public adminHandler: handlers.AdminHandler;
|
||||
private configHandler: handlers.ConfigHandler;
|
||||
private logsHandler: handlers.LogsHandler;
|
||||
private securityHandler: handlers.SecurityHandler;
|
||||
private statsHandler: handlers.StatsHandler;
|
||||
private radiusHandler: handlers.RadiusHandler;
|
||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||
private certificateHandler: handlers.CertificateHandler;
|
||||
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
||||
private routeManagementHandler: handlers.RouteManagementHandler;
|
||||
private apiTokenHandler: handlers.ApiTokenHandler;
|
||||
public adminHandler!: handlers.AdminHandler;
|
||||
private configHandler!: handlers.ConfigHandler;
|
||||
private logsHandler!: handlers.LogsHandler;
|
||||
private securityHandler!: handlers.SecurityHandler;
|
||||
private statsHandler!: handlers.StatsHandler;
|
||||
private radiusHandler!: handlers.RadiusHandler;
|
||||
private emailOpsHandler!: handlers.EmailOpsHandler;
|
||||
private certificateHandler!: handlers.CertificateHandler;
|
||||
private remoteIngressHandler!: handlers.RemoteIngressHandler;
|
||||
private routeManagementHandler!: handlers.RouteManagementHandler;
|
||||
private apiTokenHandler!: handlers.ApiTokenHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
@@ -39,7 +39,7 @@ export class OpsServer {
|
||||
public async start() {
|
||||
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
||||
domain: 'localhost',
|
||||
feedMetadata: null,
|
||||
feedMetadata: undefined,
|
||||
serveDir: paths.distServe,
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ export class OpsServer {
|
||||
// Set up handlers
|
||||
await this.setupHandlers();
|
||||
|
||||
await this.server.start(3000);
|
||||
await this.server.start(this.dcRouterRef.options.opsServerPort ?? 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ export class AdminHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
// JWT instance
|
||||
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||
|
||||
// Simple in-memory user storage (in production, use proper database)
|
||||
private users = new Map<string, {
|
||||
|
||||
@@ -311,8 +311,8 @@ export class CertificateHandler {
|
||||
}
|
||||
}
|
||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||
} catch (err) {
|
||||
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,8 +340,8 @@ export class CertificateHandler {
|
||||
try {
|
||||
await dcRouter.smartAcme.getCertificateForDomain(domain);
|
||||
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
|
||||
} catch (err) {
|
||||
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +351,8 @@ export class CertificateHandler {
|
||||
try {
|
||||
await smartProxy.provisionCertificate(routeNames[0]);
|
||||
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
|
||||
} catch (err) {
|
||||
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ export class RadiusHandler {
|
||||
try {
|
||||
await radiusServer.addClient(dataArg.client);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
} catch (error: unknown) {
|
||||
return { success: false, message: (error as Error).message };
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -144,8 +144,8 @@ export class RadiusHandler {
|
||||
updatedAt: mapping.updatedAt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
} catch (error: unknown) {
|
||||
return { success: false, message: (error as Error).message };
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -101,6 +101,7 @@ export class SecurityHandler {
|
||||
throughputByIP,
|
||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||
requestsTotal: networkStats.requestsTotal || 0,
|
||||
backends: networkStats.backends || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,6 +115,7 @@ export class SecurityHandler {
|
||||
throughputByIP: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
backends: [],
|
||||
};
|
||||
}
|
||||
)
|
||||
|
||||
@@ -279,7 +279,7 @@ export class StatsHandler {
|
||||
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
const stats = await this.opsServerRef.dcRouterRef.metricsManager!.getNetworkStats();
|
||||
const serverStats = await this.collectServerStats();
|
||||
|
||||
// Build per-IP bandwidth lookup from throughputByIP
|
||||
@@ -309,6 +309,7 @@ export class StatsHandler {
|
||||
throughputHistory: stats.throughputHistory || [],
|
||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||
requestsTotal: stats.requestsTotal || 0,
|
||||
backends: stats.backends || [],
|
||||
};
|
||||
})()
|
||||
);
|
||||
@@ -489,44 +490,41 @@ export class StatsHandler {
|
||||
message?: string;
|
||||
}>;
|
||||
}> {
|
||||
const services: Array<{
|
||||
name: string;
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
message?: string;
|
||||
}> = [];
|
||||
|
||||
// Check HTTP Proxy
|
||||
if (this.opsServerRef.dcRouterRef.smartProxy) {
|
||||
services.push({
|
||||
name: 'HTTP/HTTPS Proxy',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check Email Server
|
||||
if (this.opsServerRef.dcRouterRef.emailServer) {
|
||||
services.push({
|
||||
name: 'Email Server',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check DNS Server
|
||||
if (this.opsServerRef.dcRouterRef.dnsServer) {
|
||||
services.push({
|
||||
name: 'DNS Server',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check OpsServer
|
||||
services.push({
|
||||
name: 'OpsServer',
|
||||
status: 'healthy',
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const health = dcRouter.serviceManager.getHealth();
|
||||
|
||||
const services = health.services.map((svc) => {
|
||||
let status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
switch (svc.state) {
|
||||
case 'running':
|
||||
status = 'healthy';
|
||||
break;
|
||||
case 'starting':
|
||||
case 'degraded':
|
||||
status = 'degraded';
|
||||
break;
|
||||
case 'failed':
|
||||
status = svc.criticality === 'critical' ? 'unhealthy' : 'degraded';
|
||||
break;
|
||||
case 'stopped':
|
||||
case 'stopping':
|
||||
default:
|
||||
status = 'degraded';
|
||||
break;
|
||||
}
|
||||
|
||||
let message: string | undefined;
|
||||
if (svc.state === 'failed' && svc.lastError) {
|
||||
message = svc.lastError;
|
||||
} else if (svc.retryCount > 0 && svc.state !== 'running') {
|
||||
message = `Retry attempt ${svc.retryCount}`;
|
||||
}
|
||||
|
||||
return { name: svc.name, status, message };
|
||||
});
|
||||
|
||||
const healthy = services.every(s => s.status === 'healthy');
|
||||
|
||||
|
||||
const healthy = health.overall === 'healthy';
|
||||
|
||||
return {
|
||||
healthy,
|
||||
services,
|
||||
|
||||
@@ -62,8 +62,9 @@ import * as smartradius from '@push.rocks/smartradius';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartrx from '@push.rocks/smartrx';
|
||||
import * as smartunique from '@push.rocks/smartunique';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmetrics, smartmongo, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique };
|
||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmetrics, smartmongo, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, taskbuffer };
|
||||
|
||||
// Define SmartLog types for use in error handling
|
||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||
|
||||
@@ -92,6 +92,8 @@ export interface IAccountingManagerConfig {
|
||||
detailedLogging?: boolean;
|
||||
/** Maximum active sessions to track in memory */
|
||||
maxActiveSessions?: number;
|
||||
/** Stale session timeout in hours — sessions with no update for this long are evicted (default: 24) */
|
||||
staleSessionTimeoutHours?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +107,7 @@ export class AccountingManager {
|
||||
private activeSessions: Map<string, IAccountingSession> = new Map();
|
||||
private config: Required<IAccountingManagerConfig>;
|
||||
private storageManager?: StorageManager;
|
||||
private staleSessionSweepTimer?: ReturnType<typeof setInterval>;
|
||||
|
||||
// Counters for statistics
|
||||
private stats = {
|
||||
@@ -121,6 +124,7 @@ export class AccountingManager {
|
||||
retentionDays: config?.retentionDays ?? 30,
|
||||
detailedLogging: config?.detailedLogging ?? false,
|
||||
maxActiveSessions: config?.maxActiveSessions ?? 10000,
|
||||
staleSessionTimeoutHours: config?.staleSessionTimeoutHours ?? 24,
|
||||
};
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
@@ -132,9 +136,60 @@ export class AccountingManager {
|
||||
if (this.storageManager) {
|
||||
await this.loadActiveSessions();
|
||||
}
|
||||
|
||||
// Start periodic sweep to evict stale sessions (every 15 minutes)
|
||||
this.staleSessionSweepTimer = setInterval(() => {
|
||||
this.sweepStaleSessions();
|
||||
}, 15 * 60 * 1000);
|
||||
// Allow the process to exit even if the timer is pending
|
||||
if (this.staleSessionSweepTimer.unref) {
|
||||
this.staleSessionSweepTimer.unref();
|
||||
}
|
||||
|
||||
logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the accounting manager and clean up timers
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.staleSessionSweepTimer) {
|
||||
clearInterval(this.staleSessionSweepTimer);
|
||||
this.staleSessionSweepTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweep stale active sessions that have not received any update
|
||||
* within the configured timeout. These are orphaned sessions where
|
||||
* the Stop packet was never received.
|
||||
*/
|
||||
private sweepStaleSessions(): void {
|
||||
const timeoutMs = this.config.staleSessionTimeoutHours * 60 * 60 * 1000;
|
||||
const cutoff = Date.now() - timeoutMs;
|
||||
let swept = 0;
|
||||
|
||||
for (const [sessionId, session] of this.activeSessions) {
|
||||
if (session.lastUpdateTime < cutoff) {
|
||||
session.status = 'terminated';
|
||||
session.terminateCause = 'StaleSessionTimeout';
|
||||
session.endTime = Date.now();
|
||||
session.sessionTime = Math.floor((session.endTime - session.startTime) / 1000);
|
||||
|
||||
if (this.storageManager) {
|
||||
this.archiveSession(session).catch(() => {});
|
||||
}
|
||||
|
||||
this.activeSessions.delete(sessionId);
|
||||
swept++;
|
||||
}
|
||||
}
|
||||
|
||||
if (swept > 0) {
|
||||
logger.log('info', `Swept ${swept} stale RADIUS sessions (no update for ${this.config.staleSessionTimeoutHours}h)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle accounting start request
|
||||
*/
|
||||
@@ -463,8 +518,8 @@ export class AccountingManager {
|
||||
if (deletedCount > 0) {
|
||||
logger.log('info', `Cleaned up ${deletedCount} old accounting sessions`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to cleanup old sessions: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to cleanup old sessions: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
@@ -527,8 +582,8 @@ export class AccountingManager {
|
||||
// Ignore individual errors
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to load active sessions: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('warn', `Failed to load active sessions: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,8 +598,8 @@ export class AccountingManager {
|
||||
const key = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
|
||||
try {
|
||||
await this.storageManager.setJSON(key, session);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to persist session ${session.sessionId}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to persist session ${session.sessionId}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,8 +620,8 @@ export class AccountingManager {
|
||||
const date = new Date(session.endTime);
|
||||
const archiveKey = `${this.config.storagePrefix}/archive/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${session.sessionId}.json`;
|
||||
await this.storageManager.setJSON(archiveKey, session);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to archive session ${session.sessionId}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to archive session ${session.sessionId}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,8 +653,8 @@ export class AccountingManager {
|
||||
// Ignore individual errors
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to get archived sessions: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('warn', `Failed to get archived sessions: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return sessions;
|
||||
|
||||
@@ -183,6 +183,8 @@ export class RadiusServer {
|
||||
this.radiusServer = undefined;
|
||||
}
|
||||
|
||||
this.accountingManager.stop();
|
||||
|
||||
this.running = false;
|
||||
logger.log('info', 'RADIUS server stopped');
|
||||
}
|
||||
@@ -308,8 +310,8 @@ export class RadiusServer {
|
||||
default:
|
||||
logger.log('debug', `RADIUS Acct Unknown status type: ${statusType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `RADIUS accounting error: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `RADIUS accounting error: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };
|
||||
|
||||
@@ -104,7 +104,7 @@ export class VlanManager {
|
||||
if (this.normalizedMacCache.size > 10000) {
|
||||
const iterator = this.normalizedMacCache.keys();
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
this.normalizedMacCache.delete(iterator.next().value);
|
||||
this.normalizedMacCache.delete(iterator.next().value!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,8 +348,8 @@ export class VlanManager {
|
||||
}
|
||||
logger.log('info', `Loaded ${data.length} VLAN mappings from storage`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to load VLAN mappings from storage: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('warn', `Failed to load VLAN mappings from storage: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,8 +364,8 @@ export class VlanManager {
|
||||
try {
|
||||
const mappings = Array.from(this.mappings.values());
|
||||
await this.storageManager.setJSON(this.config.storagePrefix, mappings);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to save VLAN mappings to storage: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to save VLAN mappings to storage: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
ts/readme.md
16
ts/readme.md
@@ -37,7 +37,7 @@ const router = new DcRouter({
|
||||
});
|
||||
|
||||
await router.start();
|
||||
// OpsServer dashboard at http://localhost:3000
|
||||
// OpsServer dashboard at http://localhost:3000 (configurable via opsServerPort)
|
||||
|
||||
// Graceful shutdown
|
||||
await router.stop();
|
||||
@@ -60,6 +60,9 @@ ts/
|
||||
│ └── documents/ # Cached document models
|
||||
├── config/ # Configuration utilities
|
||||
├── errors/ # Error classes and retry logic
|
||||
├── http3/ # HTTP/3 (QUIC) route augmentation
|
||||
│ ├── index.ts # Barrel export
|
||||
│ └── http3-route-augmentation.ts # Pure utility: augmentRoutesWithHttp3(), IHttp3Config
|
||||
├── monitoring/ # MetricsManager (SmartMetrics integration)
|
||||
├── opsserver/ # OpsServer dashboard + API handlers
|
||||
│ ├── classes.opsserver.ts # HTTP server + TypedRouter setup
|
||||
@@ -71,7 +74,10 @@ ts/
|
||||
│ ├── email.handler.ts # Email operations
|
||||
│ ├── certificate.handler.ts # Certificate management
|
||||
│ ├── radius.handler.ts # RADIUS management
|
||||
│ └── remoteingress.handler.ts # Remote ingress edge + token management
|
||||
│ ├── remoteingress.handler.ts # Remote ingress edge + token management
|
||||
│ ├── route-management.handler.ts # Programmatic route CRUD
|
||||
│ ├── api-token.handler.ts # API token management
|
||||
│ └── security.handler.ts # Security metrics + connections
|
||||
├── radius/ # RADIUS server integration
|
||||
├── remoteingress/ # Remote ingress hub integration
|
||||
│ ├── classes.remoteingress-manager.ts # Edge CRUD + port derivation
|
||||
@@ -96,6 +102,9 @@ export { RadiusServer, IRadiusServerConfig } from './radius/index.js';
|
||||
|
||||
// Remote Ingress
|
||||
export { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||
|
||||
// HTTP/3
|
||||
export type { IHttp3Config } from './http3/index.js';
|
||||
```
|
||||
|
||||
## Key Classes
|
||||
@@ -112,6 +121,7 @@ The central orchestrator. Accepts `IDcRouterOptions` and manages the lifecycle o
|
||||
| `radiusConfig` | RadiusServer (auth + accounting) | `@push.rocks/smartradius` |
|
||||
| `remoteIngressConfig` | RemoteIngressManager + TunnelManager | `@serve.zone/remoteingress` |
|
||||
| `tls` + `dnsChallenge` | SmartAcme (ACME cert provisioning) | `@push.rocks/smartacme` |
|
||||
| `http3` | HTTP/3 route augmentation (enabled by default) | built-in |
|
||||
| `cacheConfig` | CacheDb (embedded MongoDB) | `@push.rocks/smartdata` |
|
||||
| *(always)* | OpsServer (dashboard + API) | `@api.global/typedserver` |
|
||||
| *(always)* | MetricsManager | `@push.rocks/smartmetrics` |
|
||||
@@ -126,7 +136,7 @@ Manages the Rust-based RemoteIngressHub lifecycle. Syncs allowed edges, tracks c
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const STORAGE_PREFIX = '/remote-ingress/';
|
||||
/**
|
||||
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
||||
*/
|
||||
function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] {
|
||||
function extractPorts(portRange: number | Array<number | { from: number; to: number }>): number[] {
|
||||
const ports = new Set<number>();
|
||||
if (typeof portRange === 'number') {
|
||||
ports.add(portRange);
|
||||
@@ -94,6 +94,38 @@ export class RemoteIngressManager {
|
||||
return [...ports].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive UDP listen ports for an edge from routes with transport 'udp' or 'all'.
|
||||
* These ports need UDP listeners on the edge (e.g. for QUIC/HTTP3).
|
||||
*/
|
||||
public deriveUdpPortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
|
||||
const ports = new Set<number>();
|
||||
|
||||
for (const route of this.routes) {
|
||||
if (!route.remoteIngress?.enabled) continue;
|
||||
|
||||
// Apply edge filter if present
|
||||
const filter = route.remoteIngress.edgeFilter;
|
||||
if (filter && filter.length > 0) {
|
||||
const idMatch = filter.includes(edgeId);
|
||||
const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
|
||||
if (!idMatch && !tagMatch) continue;
|
||||
}
|
||||
|
||||
// Only include ports from routes that listen on UDP
|
||||
const transport = route.match?.transport;
|
||||
if (transport === 'udp' || transport === 'all') {
|
||||
if (route.match?.ports) {
|
||||
for (const p of extractPorts(route.match.ports)) {
|
||||
ports.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...ports].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective listen ports for an edge.
|
||||
* Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true.
|
||||
@@ -106,6 +138,18 @@ export class RemoteIngressManager {
|
||||
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective UDP listen ports for an edge.
|
||||
* Manual UDP ports are always included. Auto-derived UDP ports are added when autoDerivePorts is true.
|
||||
*/
|
||||
public getEffectiveListenPortsUdp(edge: IRemoteIngress): number[] {
|
||||
const manualPorts = edge.listenPortsUdp || [];
|
||||
const shouldDerive = edge.autoDerivePorts !== false;
|
||||
if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b);
|
||||
const derivedPorts = this.deriveUdpPortsForEdge(edge.id, edge.tags);
|
||||
return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual and derived port breakdown for an edge (used in API responses).
|
||||
* Derived ports exclude any ports already present in the manual list.
|
||||
@@ -241,15 +285,18 @@ export class RemoteIngressManager {
|
||||
|
||||
/**
|
||||
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
||||
*/
|
||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[] }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[] }> = [];
|
||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
|
||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
|
||||
for (const edge of this.edges.values()) {
|
||||
if (edge.enabled) {
|
||||
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||
result.push({
|
||||
id: edge.id,
|
||||
secret: edge.secret,
|
||||
listenPorts: this.getEffectiveListenPorts(edge),
|
||||
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export enum ThreatCategory {
|
||||
* Content Scanner for detecting malicious email content
|
||||
*/
|
||||
export class ContentScanner {
|
||||
private static instance: ContentScanner;
|
||||
private static instance: ContentScanner | undefined;
|
||||
private scanCache: LRUCache<string, IScanResult>;
|
||||
private options: Required<IContentScannerOptions>;
|
||||
|
||||
@@ -258,12 +258,12 @@ export class ContentScanner {
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error scanning email: ${error.message}`, {
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error scanning email: ${(error as Error).message}`, {
|
||||
messageId: email.getMessageId(),
|
||||
error: error.stack
|
||||
error: (error as Error).stack
|
||||
});
|
||||
|
||||
|
||||
// Return a safe default with error indication
|
||||
return {
|
||||
isClean: true, // Let it pass if scanner fails (configure as desired)
|
||||
@@ -271,7 +271,7 @@ export class ContentScanner {
|
||||
scannedElements: ['error'],
|
||||
timestamp: Date.now(),
|
||||
threatType: 'scan_error',
|
||||
threatDetails: `Scan error: ${error.message}`
|
||||
threatDetails: `Scan error: ${(error as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -625,8 +625,8 @@ export class ContentScanner {
|
||||
return sample.toString('utf8')
|
||||
.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
|
||||
.replace(/\uFFFD/g, ''); // Remove replacement char
|
||||
} catch (error) {
|
||||
logger.log('warn', `Error extracting text from buffer: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('warn', `Error extracting text from buffer: ${(error as Error).message}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -699,10 +699,10 @@ export class ContentScanner {
|
||||
subject: email.subject
|
||||
},
|
||||
success: false,
|
||||
domain: email.getFromDomain()
|
||||
domain: email.getFromDomain() ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a threat finding to the security logger
|
||||
* @param email The email containing the threat
|
||||
@@ -722,10 +722,10 @@ export class ContentScanner {
|
||||
subject: email.subject
|
||||
},
|
||||
success: false,
|
||||
domain: email.getFromDomain()
|
||||
domain: email.getFromDomain() ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get threat level description based on score
|
||||
* @param score Threat score
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface IIPReputationOptions {
|
||||
* Class for checking IP reputation of inbound email senders
|
||||
*/
|
||||
export class IPReputationChecker {
|
||||
private static instance: IPReputationChecker;
|
||||
private static instance: IPReputationChecker | undefined;
|
||||
private reputationCache: LRUCache<string, IReputationResult>;
|
||||
private options: Required<IIPReputationOptions>;
|
||||
private storageManager?: any; // StorageManager instance
|
||||
@@ -127,8 +127,8 @@ export class IPReputationChecker {
|
||||
// Load cache from disk if enabled
|
||||
if (this.options.enableLocalCache) {
|
||||
// Fire and forget the load operation
|
||||
this.loadCache().catch(error => {
|
||||
logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`);
|
||||
this.loadCache().catch((error: unknown) => {
|
||||
logger.log('error', `Failed to load IP reputation cache during initialization: ${(error as Error).message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -237,13 +237,13 @@ export class IPReputationChecker {
|
||||
this.logReputationCheck(ip, result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error checking IP reputation for ${ip}: ${(error as Error).message}`, {
|
||||
ip,
|
||||
stack: error.stack
|
||||
stack: (error as Error).stack
|
||||
});
|
||||
|
||||
return this.createErrorResult(ip, error.message);
|
||||
|
||||
return this.createErrorResult(ip, (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +266,8 @@ export class IPReputationChecker {
|
||||
const lookupDomain = `${reversedIP}.${server}`;
|
||||
await plugins.dns.promises.resolve(lookupDomain);
|
||||
return server; // IP is listed in this DNSBL
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOTFOUND') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as any).code === 'ENOTFOUND') {
|
||||
return null; // IP is not listed in this DNSBL
|
||||
}
|
||||
throw error; // Other error
|
||||
@@ -286,8 +286,8 @@ export class IPReputationChecker {
|
||||
listCount: lists.length,
|
||||
lists
|
||||
};
|
||||
} catch (error) {
|
||||
logger.log('error', `Error checking DNSBL for ${ip}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error checking DNSBL for ${ip}: ${(error as Error).message}`);
|
||||
return {
|
||||
listCount: 0,
|
||||
lists: []
|
||||
@@ -349,8 +349,8 @@ export class IPReputationChecker {
|
||||
org: this.determineOrg(ip), // Simplified, would use real org data
|
||||
type
|
||||
};
|
||||
} catch (error) {
|
||||
logger.log('error', `Error getting IP info for ${ip}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error getting IP info for ${ip}: ${(error as Error).message}`);
|
||||
return {
|
||||
type: IPType.UNKNOWN
|
||||
};
|
||||
@@ -468,8 +468,8 @@ export class IPReputationChecker {
|
||||
}
|
||||
this.saveCacheTimer = setTimeout(() => {
|
||||
this.saveCacheTimer = null;
|
||||
this.saveCache().catch(error => {
|
||||
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
||||
this.saveCache().catch((error: unknown) => {
|
||||
logger.log('error', `Failed to save IP reputation cache: ${(error as Error).message}`);
|
||||
});
|
||||
}, IPReputationChecker.SAVE_CACHE_DEBOUNCE_MS);
|
||||
}
|
||||
@@ -506,11 +506,11 @@ export class IPReputationChecker {
|
||||
|
||||
logger.log('info', `Saved ${entries.length} IP reputation cache entries to disk`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to save IP reputation cache: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load cache from disk or storage manager
|
||||
*/
|
||||
@@ -542,12 +542,12 @@ export class IPReputationChecker {
|
||||
plugins.fs.unlinkSync(cacheFile);
|
||||
logger.log('info', 'Old cache file removed after migration');
|
||||
} catch (deleteError) {
|
||||
logger.log('warn', `Could not delete old cache file: ${deleteError.message}`);
|
||||
logger.log('warn', `Could not delete old cache file: ${(deleteError as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Error loading from StorageManager: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Error loading from StorageManager: ${(error as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
// No storage manager, load from filesystem
|
||||
@@ -578,8 +578,8 @@ export class IPReputationChecker {
|
||||
const source = fromFilesystem ? 'disk' : 'StorageManager';
|
||||
logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from ${source}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to load IP reputation cache: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to load IP reputation cache: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,8 +611,8 @@ export class IPReputationChecker {
|
||||
|
||||
// If cache is enabled and we have entries, save them to the new storage manager
|
||||
if (this.options.enableLocalCache && this.reputationCache.size > 0) {
|
||||
this.saveCache().catch(error => {
|
||||
logger.log('error', `Failed to save cache to new storage manager: ${error.message}`);
|
||||
this.saveCache().catch((error: unknown) => {
|
||||
logger.log('error', `Failed to save cache to new storage manager: ${(error as Error).message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface ISecurityEvent {
|
||||
* Security logger for enhanced security monitoring
|
||||
*/
|
||||
export class SecurityLogger {
|
||||
private static instance: SecurityLogger;
|
||||
private static instance: SecurityLogger | undefined;
|
||||
private securityEvents: ISecurityEvent[] = [];
|
||||
private maxEventHistory: number;
|
||||
private enableNotifications: boolean;
|
||||
@@ -154,11 +154,13 @@ export class SecurityLogger {
|
||||
}
|
||||
|
||||
if (filter.fromTimestamp) {
|
||||
filteredEvents = filteredEvents.filter(event => event.timestamp >= filter.fromTimestamp);
|
||||
const fromTs = filter.fromTimestamp;
|
||||
filteredEvents = filteredEvents.filter(event => event.timestamp >= fromTs);
|
||||
}
|
||||
|
||||
|
||||
if (filter.toTimestamp) {
|
||||
filteredEvents = filteredEvents.filter(event => event.timestamp <= filter.toTimestamp);
|
||||
const toTs = filter.toTimestamp;
|
||||
filteredEvents = filteredEvents.filter(event => event.timestamp <= toTs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { smsConfigSchema } from './config/sms.schema.js';
|
||||
import { ConfigValidator } from '../config/validator.js';
|
||||
|
||||
export class SmsService {
|
||||
public projectinfo: plugins.projectinfo.ProjectInfo;
|
||||
public projectinfo!: plugins.projectinfo.ProjectInfo;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public config: ISmsConfig;
|
||||
|
||||
@@ -16,7 +16,7 @@ export class SmsService {
|
||||
const validationResult = ConfigValidator.validate(options, smsConfigSchema);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors.join(', ')}`);
|
||||
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors!.join(', ')}`);
|
||||
}
|
||||
|
||||
// Set configuration with defaults
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface IStorageConfig {
|
||||
/** Filesystem path for storage */
|
||||
fsPath?: string;
|
||||
/** Custom read function */
|
||||
readFunction?: (key: string) => Promise<string>;
|
||||
readFunction?: (key: string) => Promise<string | null>;
|
||||
/** Custom write function */
|
||||
writeFunction?: (key: string, value: string) => Promise<void>;
|
||||
}
|
||||
@@ -57,9 +57,7 @@ export class StorageManager {
|
||||
this.ensureDirectory(this.fsBasePath);
|
||||
|
||||
// Set up internal filesystem read/write functions
|
||||
this.config.readFunction = async (key: string) => {
|
||||
return this.fsRead(key);
|
||||
};
|
||||
this.config.readFunction = (key: string): Promise<string | null> => this.fsRead(key);
|
||||
this.config.writeFunction = async (key: string, value: string) => {
|
||||
await this.fsWrite(key, value);
|
||||
};
|
||||
@@ -88,8 +86,8 @@ export class StorageManager {
|
||||
private async ensureDirectory(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await plugins.fsUtils.ensureDir(dirPath);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to create storage directory: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to create storage directory: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -129,19 +127,19 @@ export class StorageManager {
|
||||
/**
|
||||
* Internal filesystem read function
|
||||
*/
|
||||
private async fsRead(key: string): Promise<string> {
|
||||
private async fsRead(key: string): Promise<string | null> {
|
||||
const filePath = this.keyToPath(key);
|
||||
try {
|
||||
const content = await readFile(filePath, 'utf8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal filesystem write function
|
||||
*/
|
||||
@@ -186,8 +184,8 @@ export class StorageManager {
|
||||
default:
|
||||
throw new Error(`Unknown backend: ${this.backend}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Storage get error for key ${key}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Storage get error for key ${key}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +228,7 @@ export class StorageManager {
|
||||
this.memoryStore.set(key, value);
|
||||
// Evict oldest entries if memory store exceeds limit
|
||||
while (this.memoryStore.size > StorageManager.MAX_MEMORY_ENTRIES) {
|
||||
const firstKey = this.memoryStore.keys().next().value;
|
||||
const firstKey = this.memoryStore.keys().next().value!;
|
||||
this.memoryStore.delete(firstKey);
|
||||
}
|
||||
break;
|
||||
@@ -239,8 +237,8 @@ export class StorageManager {
|
||||
default:
|
||||
throw new Error(`Unknown backend: ${this.backend}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Storage set error for key ${key}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Storage set error for key ${key}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -257,8 +255,8 @@ export class StorageManager {
|
||||
const filePath = this.keyToPath(key);
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -281,8 +279,8 @@ export class StorageManager {
|
||||
default:
|
||||
throw new Error(`Unknown backend: ${this.backend}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Storage delete error for key ${key}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Storage delete error for key ${key}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -319,8 +317,8 @@ export class StorageManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -348,8 +346,8 @@ export class StorageManager {
|
||||
default:
|
||||
throw new Error(`Unknown backend: ${this.backend}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Storage list error for prefix ${prefix}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Storage list error for prefix ${prefix}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -390,8 +388,8 @@ export class StorageManager {
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to parse JSON for key ${key}: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
logger.log('error', `Failed to parse JSON for key ${key}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
157
ts_apiclient/classes.apitoken.ts
Normal file
157
ts_apiclient/classes.apitoken.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class ApiToken {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IApiTokenInfo
|
||||
public id: string;
|
||||
public name: string;
|
||||
public scopes: interfaces.data.TApiTokenScope[];
|
||||
public createdAt: number;
|
||||
public expiresAt: number | null;
|
||||
public lastUsedAt: number | null;
|
||||
public enabled: boolean;
|
||||
|
||||
/** Only set on creation or roll. Not persisted on server side. */
|
||||
public tokenValue?: string;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IApiTokenInfo, tokenValue?: string) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.scopes = data.scopes;
|
||||
this.createdAt = data.createdAt;
|
||||
this.expiresAt = data.expiresAt;
|
||||
this.lastUsedAt = data.lastUsedAt;
|
||||
this.enabled = data.enabled;
|
||||
this.tokenValue = tokenValue;
|
||||
}
|
||||
|
||||
public async revoke(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RevokeApiToken>(
|
||||
'revokeApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to revoke token');
|
||||
}
|
||||
}
|
||||
|
||||
public async roll(): Promise<string> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RollApiToken>(
|
||||
'rollApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to roll token');
|
||||
}
|
||||
this.tokenValue = response.tokenValue;
|
||||
return response.tokenValue!;
|
||||
}
|
||||
|
||||
public async toggle(enabled: boolean): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleApiToken>(
|
||||
'toggleApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to toggle token');
|
||||
}
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiTokenBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private tokenName: string = '';
|
||||
private tokenScopes: interfaces.data.TApiTokenScope[] = [];
|
||||
private tokenExpiresInDays?: number | null;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.tokenName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setScopes(scopes: interfaces.data.TApiTokenScope[]): this {
|
||||
this.tokenScopes = scopes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public addScope(scope: interfaces.data.TApiTokenScope): this {
|
||||
if (!this.tokenScopes.includes(scope)) {
|
||||
this.tokenScopes.push(scope);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public setExpiresInDays(days: number | null): this {
|
||||
this.tokenExpiresInDays = days;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<ApiToken> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateApiToken>(
|
||||
'createApiToken',
|
||||
this.clientRef.buildRequestPayload({
|
||||
name: this.tokenName,
|
||||
scopes: this.tokenScopes,
|
||||
expiresInDays: this.tokenExpiresInDays,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create API token');
|
||||
}
|
||||
return new ApiToken(
|
||||
this.clientRef,
|
||||
{
|
||||
id: response.tokenId!,
|
||||
name: this.tokenName,
|
||||
scopes: this.tokenScopes,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: this.tokenExpiresInDays
|
||||
? Date.now() + this.tokenExpiresInDays * 24 * 60 * 60 * 1000
|
||||
: null,
|
||||
lastUsedAt: null,
|
||||
enabled: true,
|
||||
},
|
||||
response.tokenValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiTokenManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<ApiToken[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ListApiTokens>(
|
||||
'listApiTokens',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.tokens.map((t) => new ApiToken(this.clientRef, t));
|
||||
}
|
||||
|
||||
public async create(options: {
|
||||
name: string;
|
||||
scopes: interfaces.data.TApiTokenScope[];
|
||||
expiresInDays?: number | null;
|
||||
}): Promise<ApiToken> {
|
||||
return this.build()
|
||||
.setName(options.name)
|
||||
.setScopes(options.scopes)
|
||||
.setExpiresInDays(options.expiresInDays ?? null)
|
||||
.save();
|
||||
}
|
||||
|
||||
public build(): ApiTokenBuilder {
|
||||
return new ApiTokenBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
123
ts_apiclient/classes.certificate.ts
Normal file
123
ts_apiclient/classes.certificate.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Certificate {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from ICertificateInfo
|
||||
public domain: string;
|
||||
public routeNames: string[];
|
||||
public status: interfaces.requests.TCertificateStatus;
|
||||
public source: interfaces.requests.TCertificateSource;
|
||||
public tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||
public expiryDate?: string;
|
||||
public issuer?: string;
|
||||
public issuedAt?: string;
|
||||
public error?: string;
|
||||
public canReprovision: boolean;
|
||||
public backoffInfo?: {
|
||||
failures: number;
|
||||
retryAfter?: string;
|
||||
lastError?: string;
|
||||
};
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.ICertificateInfo) {
|
||||
this.clientRef = clientRef;
|
||||
this.domain = data.domain;
|
||||
this.routeNames = data.routeNames;
|
||||
this.status = data.status;
|
||||
this.source = data.source;
|
||||
this.tlsMode = data.tlsMode;
|
||||
this.expiryDate = data.expiryDate;
|
||||
this.issuer = data.issuer;
|
||||
this.issuedAt = data.issuedAt;
|
||||
this.error = data.error;
|
||||
this.canReprovision = data.canReprovision;
|
||||
this.backoffInfo = data.backoffInfo;
|
||||
}
|
||||
|
||||
public async reprovision(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||
'reprovisionCertificateDomain',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to reprovision certificate');
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteCertificate>(
|
||||
'deleteCertificate',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete certificate');
|
||||
}
|
||||
}
|
||||
|
||||
public async export(): Promise<{
|
||||
id: string;
|
||||
domainName: string;
|
||||
created: number;
|
||||
validUntil: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
csr: string;
|
||||
} | undefined> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ExportCertificate>(
|
||||
'exportCertificate',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to export certificate');
|
||||
}
|
||||
return response.cert;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICertificateSummary {
|
||||
total: number;
|
||||
valid: number;
|
||||
expiring: number;
|
||||
expired: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
}
|
||||
|
||||
export class CertificateManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<{ certificates: Certificate[]; summary: ICertificateSummary }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetCertificateOverview>(
|
||||
'getCertificateOverview',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return {
|
||||
certificates: response.certificates.map((c) => new Certificate(this.clientRef, c)),
|
||||
summary: response.summary,
|
||||
};
|
||||
}
|
||||
|
||||
public async import(cert: {
|
||||
id: string;
|
||||
domainName: string;
|
||||
created: number;
|
||||
validUntil: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
csr: string;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ImportCertificate>(
|
||||
'importCertificate',
|
||||
this.clientRef.buildRequestPayload({ cert }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to import certificate');
|
||||
}
|
||||
}
|
||||
}
|
||||
17
ts_apiclient/classes.config.ts
Normal file
17
ts_apiclient/classes.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class ConfigManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async get(section?: string): Promise<interfaces.requests.IReq_GetConfiguration['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetConfiguration>(
|
||||
'getConfiguration',
|
||||
this.clientRef.buildRequestPayload({ section }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
import { RouteManager } from './classes.route.js';
|
||||
import { CertificateManager } from './classes.certificate.js';
|
||||
import { ApiTokenManager } from './classes.apitoken.js';
|
||||
import { RemoteIngressManager } from './classes.remoteingress.js';
|
||||
import { StatsManager } from './classes.stats.js';
|
||||
import { ConfigManager } from './classes.config.js';
|
||||
import { LogManager } from './classes.logs.js';
|
||||
import { EmailManager } from './classes.email.js';
|
||||
import { RadiusManager } from './classes.radius.js';
|
||||
|
||||
export interface IDcRouterApiClientOptions {
|
||||
baseUrl: string;
|
||||
apiToken?: string;
|
||||
}
|
||||
|
||||
export class DcRouterApiClient {
|
||||
public baseUrl: string;
|
||||
public apiToken?: string;
|
||||
public identity?: interfaces.data.IIdentity;
|
||||
|
||||
// Resource managers
|
||||
public routes: RouteManager;
|
||||
public certificates: CertificateManager;
|
||||
public apiTokens: ApiTokenManager;
|
||||
public remoteIngress: RemoteIngressManager;
|
||||
public stats: StatsManager;
|
||||
public config: ConfigManager;
|
||||
public logs: LogManager;
|
||||
public emails: EmailManager;
|
||||
public radius: RadiusManager;
|
||||
|
||||
constructor(options: IDcRouterApiClientOptions) {
|
||||
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
||||
this.apiToken = options.apiToken;
|
||||
|
||||
this.routes = new RouteManager(this);
|
||||
this.certificates = new CertificateManager(this);
|
||||
this.apiTokens = new ApiTokenManager(this);
|
||||
this.remoteIngress = new RemoteIngressManager(this);
|
||||
this.stats = new StatsManager(this);
|
||||
this.config = new ConfigManager(this);
|
||||
this.logs = new LogManager(this);
|
||||
this.emails = new EmailManager(this);
|
||||
this.radius = new RadiusManager(this);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Auth
|
||||
// =====================
|
||||
|
||||
public async login(username: string, password: string): Promise<interfaces.data.IIdentity> {
|
||||
const response = await this.request<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'adminLoginWithUsernameAndPassword',
|
||||
{ username, password },
|
||||
);
|
||||
if (response.identity) {
|
||||
this.identity = response.identity;
|
||||
}
|
||||
return response.identity!;
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
await this.request<interfaces.requests.IReq_AdminLogout>(
|
||||
'adminLogout',
|
||||
{ identity: this.identity! },
|
||||
);
|
||||
this.identity = undefined;
|
||||
}
|
||||
|
||||
public async verifyIdentity(): Promise<{ valid: boolean; identity?: interfaces.data.IIdentity }> {
|
||||
const response = await this.request<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'verifyIdentity',
|
||||
{ identity: this.identity! },
|
||||
);
|
||||
if (response.identity) {
|
||||
this.identity = response.identity;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Internal request helper
|
||||
// =====================
|
||||
|
||||
public async request<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
method: string,
|
||||
requestData: T['request'],
|
||||
): Promise<T['response']> {
|
||||
const typedRequest = new plugins.typedrequest.TypedRequest<T>(
|
||||
`${this.baseUrl}/typedrequest`,
|
||||
method,
|
||||
);
|
||||
return typedRequest.fire(requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a request payload with identity and optional API token auto-injected.
|
||||
*/
|
||||
public buildRequestPayload(extra: Record<string, any> = {}): Record<string, any> {
|
||||
const payload: Record<string, any> = { ...extra };
|
||||
if (this.identity) {
|
||||
payload.identity = this.identity;
|
||||
}
|
||||
if (this.apiToken) {
|
||||
payload.apiToken = this.apiToken;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
77
ts_apiclient/classes.email.ts
Normal file
77
ts_apiclient/classes.email.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Email {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IEmail
|
||||
public id: string;
|
||||
public direction: interfaces.requests.TEmailDirection;
|
||||
public status: interfaces.requests.TEmailStatus;
|
||||
public from: string;
|
||||
public to: string;
|
||||
public subject: string;
|
||||
public timestamp: string;
|
||||
public messageId: string;
|
||||
public size: string;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.IEmail) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.direction = data.direction;
|
||||
this.status = data.status;
|
||||
this.from = data.from;
|
||||
this.to = data.to;
|
||||
this.subject = data.subject;
|
||||
this.timestamp = data.timestamp;
|
||||
this.messageId = data.messageId;
|
||||
this.size = data.size;
|
||||
}
|
||||
|
||||
public async getDetail(): Promise<interfaces.requests.IEmailDetail | null> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||
'getEmailDetail',
|
||||
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||
);
|
||||
return response.email;
|
||||
}
|
||||
|
||||
public async resend(): Promise<{ success: boolean; newQueueId?: string }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||
'resendEmail',
|
||||
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<Email[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetAllEmails>(
|
||||
'getAllEmails',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.emails.map((e) => new Email(this.clientRef, e));
|
||||
}
|
||||
|
||||
public async getDetail(emailId: string): Promise<interfaces.requests.IEmailDetail | null> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||
'getEmailDetail',
|
||||
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||
);
|
||||
return response.email;
|
||||
}
|
||||
|
||||
public async resend(emailId: string): Promise<{ success: boolean; newQueueId?: string }> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||
'resendEmail',
|
||||
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
ts_apiclient/classes.logs.ts
Normal file
37
ts_apiclient/classes.logs.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class LogManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async getRecent(options?: {
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
timeRange?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetRecentLogs['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRecentLogs>(
|
||||
'getRecentLogs',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getStream(options?: {
|
||||
follow?: boolean;
|
||||
filters?: {
|
||||
level?: string[];
|
||||
category?: string[];
|
||||
};
|
||||
}): Promise<interfaces.requests.IReq_GetLogStream['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetLogStream>(
|
||||
'getLogStream',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
180
ts_apiclient/classes.radius.ts
Normal file
180
ts_apiclient/classes.radius.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
// =====================
|
||||
// Sub-managers
|
||||
// =====================
|
||||
|
||||
export class RadiusClientManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<Array<{
|
||||
name: string;
|
||||
ipRange: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}>> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusClients>(
|
||||
'getRadiusClients',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.clients;
|
||||
}
|
||||
|
||||
public async set(client: {
|
||||
name: string;
|
||||
ipRange: string;
|
||||
secret: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetRadiusClient>(
|
||||
'setRadiusClient',
|
||||
this.clientRef.buildRequestPayload({ client }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set RADIUS client');
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(name: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRadiusClient>(
|
||||
'removeRadiusClient',
|
||||
this.clientRef.buildRequestPayload({ name }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove RADIUS client');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RadiusVlanManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<interfaces.requests.IReq_GetVlanMappings['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetVlanMappings>(
|
||||
'getVlanMappings',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async set(mapping: {
|
||||
mac: string;
|
||||
vlan: number;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetVlanMapping>(
|
||||
'setVlanMapping',
|
||||
this.clientRef.buildRequestPayload({ mapping }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set VLAN mapping');
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(mac: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveVlanMapping>(
|
||||
'removeVlanMapping',
|
||||
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove VLAN mapping');
|
||||
}
|
||||
}
|
||||
|
||||
public async updateConfig(options: {
|
||||
defaultVlan?: number;
|
||||
allowUnknownMacs?: boolean;
|
||||
}): Promise<{ defaultVlan: number; allowUnknownMacs: boolean }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateVlanConfig>(
|
||||
'updateVlanConfig',
|
||||
this.clientRef.buildRequestPayload(options) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to update VLAN config');
|
||||
}
|
||||
return response.config;
|
||||
}
|
||||
|
||||
public async testAssignment(mac: string): Promise<interfaces.requests.IReq_TestVlanAssignment['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_TestVlanAssignment>(
|
||||
'testVlanAssignment',
|
||||
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RadiusSessionManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(filter?: {
|
||||
username?: string;
|
||||
nasIpAddress?: string;
|
||||
vlanId?: number;
|
||||
}): Promise<interfaces.requests.IReq_GetRadiusSessions['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRadiusSessions>(
|
||||
'getRadiusSessions',
|
||||
this.clientRef.buildRequestPayload({ filter }) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async disconnect(sessionId: string, reason?: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DisconnectRadiusSession>(
|
||||
'disconnectRadiusSession',
|
||||
this.clientRef.buildRequestPayload({ sessionId, reason }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to disconnect session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Main RADIUS Manager
|
||||
// =====================
|
||||
|
||||
export class RadiusManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
public clients: RadiusClientManager;
|
||||
public vlans: RadiusVlanManager;
|
||||
public sessions: RadiusSessionManager;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
this.clients = new RadiusClientManager(clientRef);
|
||||
this.vlans = new RadiusVlanManager(clientRef);
|
||||
this.sessions = new RadiusSessionManager(clientRef);
|
||||
}
|
||||
|
||||
public async getAccountingSummary(
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
): Promise<interfaces.requests.IReq_GetRadiusAccountingSummary['response']['summary']> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
||||
'getRadiusAccountingSummary',
|
||||
this.clientRef.buildRequestPayload({ startTime, endTime }) as any,
|
||||
);
|
||||
return response.summary;
|
||||
}
|
||||
|
||||
public async getStatistics(): Promise<interfaces.requests.IReq_GetRadiusStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRadiusStatistics>(
|
||||
'getRadiusStatistics',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
185
ts_apiclient/classes.remoteingress.ts
Normal file
185
ts_apiclient/classes.remoteingress.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class RemoteIngress {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IRemoteIngress
|
||||
public id: string;
|
||||
public name: string;
|
||||
public secret: string;
|
||||
public listenPorts: number[];
|
||||
public enabled: boolean;
|
||||
public autoDerivePorts: boolean;
|
||||
public tags?: string[];
|
||||
public createdAt: number;
|
||||
public updatedAt: number;
|
||||
public effectiveListenPorts?: number[];
|
||||
public manualPorts?: number[];
|
||||
public derivedPorts?: number[];
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IRemoteIngress) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.secret = data.secret;
|
||||
this.listenPorts = data.listenPorts;
|
||||
this.enabled = data.enabled;
|
||||
this.autoDerivePorts = data.autoDerivePorts;
|
||||
this.tags = data.tags;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
this.effectiveListenPorts = data.effectiveListenPorts;
|
||||
this.manualPorts = data.manualPorts;
|
||||
this.derivedPorts = data.derivedPorts;
|
||||
}
|
||||
|
||||
public async update(changes: {
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
tags?: string[];
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRemoteIngress>(
|
||||
'updateRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({ id: this.id, ...changes }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to update remote ingress');
|
||||
}
|
||||
// Update local state from response
|
||||
const edge = response.edge;
|
||||
this.name = edge.name;
|
||||
this.listenPorts = edge.listenPorts;
|
||||
this.enabled = edge.enabled;
|
||||
this.autoDerivePorts = edge.autoDerivePorts;
|
||||
this.tags = edge.tags;
|
||||
this.updatedAt = edge.updatedAt;
|
||||
this.effectiveListenPorts = edge.effectiveListenPorts;
|
||||
this.manualPorts = edge.manualPorts;
|
||||
this.derivedPorts = edge.derivedPorts;
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRemoteIngress>(
|
||||
'deleteRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete remote ingress');
|
||||
}
|
||||
}
|
||||
|
||||
public async regenerateSecret(): Promise<string> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
||||
'regenerateRemoteIngressSecret',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to regenerate secret');
|
||||
}
|
||||
this.secret = response.secret;
|
||||
return response.secret;
|
||||
}
|
||||
|
||||
public async getConnectionToken(hubHost?: string): Promise<string | undefined> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||
'getRemoteIngressConnectionToken',
|
||||
this.clientRef.buildRequestPayload({ edgeId: this.id, hubHost }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to get connection token');
|
||||
}
|
||||
return response.token;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteIngressBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private edgeName: string = '';
|
||||
private edgeListenPorts?: number[];
|
||||
private edgeAutoDerivePorts?: boolean;
|
||||
private edgeTags?: string[];
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.edgeName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setListenPorts(ports: number[]): this {
|
||||
this.edgeListenPorts = ports;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAutoDerivePorts(auto: boolean): this {
|
||||
this.edgeAutoDerivePorts = auto;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setTags(tags: string[]): this {
|
||||
this.edgeTags = tags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<RemoteIngress> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRemoteIngress>(
|
||||
'createRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({
|
||||
name: this.edgeName,
|
||||
listenPorts: this.edgeListenPorts,
|
||||
autoDerivePorts: this.edgeAutoDerivePorts,
|
||||
tags: this.edgeTags,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to create remote ingress');
|
||||
}
|
||||
return new RemoteIngress(this.clientRef, response.edge);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteIngressManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<RemoteIngress[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngresses>(
|
||||
'getRemoteIngresses',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.edges.map((e) => new RemoteIngress(this.clientRef, e));
|
||||
}
|
||||
|
||||
public async getStatuses(): Promise<interfaces.data.IRemoteIngressStatus[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
||||
'getRemoteIngressStatus',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.statuses;
|
||||
}
|
||||
|
||||
public async create(options: {
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}): Promise<RemoteIngress> {
|
||||
const builder = this.build().setName(options.name);
|
||||
if (options.listenPorts) builder.setListenPorts(options.listenPorts);
|
||||
if (options.autoDerivePorts !== undefined) builder.setAutoDerivePorts(options.autoDerivePorts);
|
||||
if (options.tags) builder.setTags(options.tags);
|
||||
return builder.save();
|
||||
}
|
||||
|
||||
public build(): RemoteIngressBuilder {
|
||||
return new RemoteIngressBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
203
ts_apiclient/classes.route.ts
Normal file
203
ts_apiclient/classes.route.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Route {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IMergedRoute
|
||||
public routeConfig: IRouteConfig;
|
||||
public source: 'hardcoded' | 'programmatic';
|
||||
public enabled: boolean;
|
||||
public overridden: boolean;
|
||||
public storedRouteId?: string;
|
||||
public createdAt?: number;
|
||||
public updatedAt?: number;
|
||||
|
||||
// Convenience accessors
|
||||
public get name(): string {
|
||||
return this.routeConfig.name || '';
|
||||
}
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IMergedRoute) {
|
||||
this.clientRef = clientRef;
|
||||
this.routeConfig = data.route;
|
||||
this.source = data.source;
|
||||
this.enabled = data.enabled;
|
||||
this.overridden = data.overridden;
|
||||
this.storedRouteId = data.storedRouteId;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
|
||||
public async update(changes: Partial<IRouteConfig>): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot update a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRoute>(
|
||||
'updateRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId, route: changes }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to update route');
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot delete a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRoute>(
|
||||
'deleteRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete route');
|
||||
}
|
||||
}
|
||||
|
||||
public async toggle(enabled: boolean): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot toggle a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleRoute>(
|
||||
'toggleRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to toggle route');
|
||||
}
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public async setOverride(enabled: boolean): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetRouteOverride>(
|
||||
'setRouteOverride',
|
||||
this.clientRef.buildRequestPayload({ routeName: this.name, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set route override');
|
||||
}
|
||||
this.overridden = true;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public async removeOverride(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRouteOverride>(
|
||||
'removeRouteOverride',
|
||||
this.clientRef.buildRequestPayload({ routeName: this.name }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove route override');
|
||||
}
|
||||
this.overridden = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private routeConfig: Partial<IRouteConfig> = {};
|
||||
private isEnabled: boolean = true;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.routeConfig.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMatch(match: IRouteConfig['match']): this {
|
||||
this.routeConfig.match = match;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAction(action: IRouteConfig['action']): this {
|
||||
this.routeConfig.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setTls(tls: IRouteConfig['action']['tls']): this {
|
||||
if (!this.routeConfig.action) {
|
||||
this.routeConfig.action = { type: 'forward' } as IRouteConfig['action'];
|
||||
}
|
||||
this.routeConfig.action!.tls = tls;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): this {
|
||||
this.isEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<Route> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||
'createRoute',
|
||||
this.clientRef.buildRequestPayload({
|
||||
route: this.routeConfig as IRouteConfig,
|
||||
enabled: this.isEnabled,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create route');
|
||||
}
|
||||
|
||||
// Return a Route instance by re-fetching the list
|
||||
// The created route is programmatic, so we find it by storedRouteId
|
||||
const { routes } = await new RouteManager(this.clientRef).list();
|
||||
const created = routes.find((r) => r.storedRouteId === response.storedRouteId);
|
||||
if (created) {
|
||||
return created;
|
||||
}
|
||||
|
||||
// Fallback: construct from known data
|
||||
return new Route(this.clientRef, {
|
||||
route: this.routeConfig as IRouteConfig,
|
||||
source: 'programmatic',
|
||||
enabled: this.isEnabled,
|
||||
overridden: false,
|
||||
storedRouteId: response.storedRouteId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<{ routes: Route[]; warnings: interfaces.data.IRouteWarning[] }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetMergedRoutes>(
|
||||
'getMergedRoutes',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return {
|
||||
routes: response.routes.map((r) => new Route(this.clientRef, r)),
|
||||
warnings: response.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
public async create(routeConfig: IRouteConfig, enabled?: boolean): Promise<Route> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||
'createRoute',
|
||||
this.clientRef.buildRequestPayload({ route: routeConfig, enabled: enabled ?? true }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create route');
|
||||
}
|
||||
return new Route(this.clientRef, {
|
||||
route: routeConfig,
|
||||
source: 'programmatic',
|
||||
enabled: enabled ?? true,
|
||||
overridden: false,
|
||||
storedRouteId: response.storedRouteId,
|
||||
});
|
||||
}
|
||||
|
||||
public build(): RouteBuilder {
|
||||
return new RouteBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
111
ts_apiclient/classes.stats.ts
Normal file
111
ts_apiclient/classes.stats.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
type TTimeRange = '1h' | '6h' | '24h' | '7d' | '30d';
|
||||
|
||||
export class StatsManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async getServer(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
includeHistory?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetServerStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetServerStatistics>(
|
||||
'getServerStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getEmail(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
domain?: string;
|
||||
includeDetails?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetEmailStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetEmailStatistics>(
|
||||
'getEmailStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getDns(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
domain?: string;
|
||||
includeQueryTypes?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetDnsStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetDnsStatistics>(
|
||||
'getDnsStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getRateLimits(options?: {
|
||||
domain?: string;
|
||||
ip?: string;
|
||||
includeBlocked?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetRateLimitStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||
'getRateLimitStatus',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getSecurity(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
includeDetails?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetSecurityMetrics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetSecurityMetrics>(
|
||||
'getSecurityMetrics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getConnections(options?: {
|
||||
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
|
||||
state?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetActiveConnections['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetActiveConnections>(
|
||||
'getActiveConnections',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getQueues(options?: {
|
||||
queueName?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetQueueStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetQueueStatus>(
|
||||
'getQueueStatus',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getHealth(detailed?: boolean): Promise<interfaces.requests.IReq_GetHealthStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'getHealthStatus',
|
||||
this.clientRef.buildRequestPayload({ detailed }) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getNetwork(): Promise<interfaces.requests.IReq_GetNetworkStats['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetNetworkStats>(
|
||||
'getNetworkStats',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getCombined(sections?: {
|
||||
server?: boolean;
|
||||
email?: boolean;
|
||||
dns?: boolean;
|
||||
security?: boolean;
|
||||
network?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetCombinedMetrics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||
'getCombinedMetrics',
|
||||
this.clientRef.buildRequestPayload({ sections }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
15
ts_apiclient/index.ts
Normal file
15
ts_apiclient/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Main client
|
||||
export { DcRouterApiClient, type IDcRouterApiClientOptions } from './classes.dcrouterapiclient.js';
|
||||
|
||||
// Resource classes
|
||||
export { Route, RouteBuilder, RouteManager } from './classes.route.js';
|
||||
export { Certificate, CertificateManager, type ICertificateSummary } from './classes.certificate.js';
|
||||
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
|
||||
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
|
||||
export { Email, EmailManager } from './classes.email.js';
|
||||
|
||||
// Read-only managers
|
||||
export { StatsManager } from './classes.stats.js';
|
||||
export { ConfigManager } from './classes.config.js';
|
||||
export { LogManager } from './classes.logs.js';
|
||||
export { RadiusManager, RadiusClientManager, RadiusVlanManager, RadiusSessionManager } from './classes.radius.js';
|
||||
8
ts_apiclient/plugins.ts
Normal file
8
ts_apiclient/plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// @api.global scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedrequestInterfaces,
|
||||
};
|
||||
279
ts_apiclient/readme.md
Normal file
279
ts_apiclient/readme.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# @serve.zone/dcrouter-apiclient
|
||||
|
||||
A typed, object-oriented API client for DcRouter with a fluent builder pattern. 🔧
|
||||
|
||||
Programmatically manage your DcRouter instance — routes, certificates, API tokens, remote ingress edges, RADIUS, email operations, and more — all with full TypeScript type safety and an intuitive OO interface.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @serve.zone/dcrouter-apiclient
|
||||
```
|
||||
|
||||
Or import directly from the main package:
|
||||
|
||||
```typescript
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
|
||||
|
||||
// Authenticate
|
||||
await client.login('admin', 'password');
|
||||
|
||||
// List routes
|
||||
const { routes, warnings } = await client.routes.list();
|
||||
console.log(`${routes.length} routes, ${warnings.length} warnings`);
|
||||
|
||||
// Check health
|
||||
const { health } = await client.stats.getHealth();
|
||||
console.log(`Healthy: ${health.healthy}`);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 🔐 Authentication
|
||||
|
||||
```typescript
|
||||
// Login with credentials — identity is stored and auto-injected into all subsequent requests
|
||||
const identity = await client.login('admin', 'password');
|
||||
|
||||
// Verify current session
|
||||
const { valid } = await client.verifyIdentity();
|
||||
|
||||
// Logout
|
||||
await client.logout();
|
||||
|
||||
// Or use an API token for programmatic access (route management only)
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://dcrouter.example.com',
|
||||
apiToken: 'dcr_your_token_here',
|
||||
});
|
||||
```
|
||||
|
||||
### 🌐 Routes — OO Resources + Builder
|
||||
|
||||
Routes are returned as `Route` instances with methods for update, delete, toggle, and overrides:
|
||||
|
||||
```typescript
|
||||
// List all routes (hardcoded + programmatic)
|
||||
const { routes, warnings } = await client.routes.list();
|
||||
|
||||
// Inspect a route
|
||||
const route = routes[0];
|
||||
console.log(route.name, route.source, route.enabled);
|
||||
|
||||
// Modify a programmatic route
|
||||
await route.update({ name: 'renamed-route' });
|
||||
await route.toggle(false);
|
||||
await route.delete();
|
||||
|
||||
// Override a hardcoded route (disable it)
|
||||
const hardcodedRoute = routes.find(r => r.source === 'hardcoded');
|
||||
await hardcodedRoute.setOverride(false);
|
||||
await hardcodedRoute.removeOverride();
|
||||
```
|
||||
|
||||
**Builder pattern** for creating new routes:
|
||||
|
||||
```typescript
|
||||
const newRoute = await client.routes.build()
|
||||
.setName('api-gateway')
|
||||
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
||||
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||
.setTls({ mode: 'terminate', certificate: 'auto' })
|
||||
.setEnabled(true)
|
||||
.save();
|
||||
|
||||
// Or use quick creation
|
||||
const route = await client.routes.create(routeConfig);
|
||||
```
|
||||
|
||||
### 🔑 API Tokens
|
||||
|
||||
```typescript
|
||||
// List existing tokens
|
||||
const tokens = await client.apiTokens.list();
|
||||
|
||||
// Create with builder
|
||||
const token = await client.apiTokens.build()
|
||||
.setName('ci-pipeline')
|
||||
.setScopes(['routes:read', 'routes:write'])
|
||||
.addScope('config:read')
|
||||
.setExpiresInDays(90)
|
||||
.save();
|
||||
|
||||
console.log(token.tokenValue); // Only available at creation time!
|
||||
|
||||
// Manage tokens
|
||||
await token.toggle(false); // Disable
|
||||
const newValue = await token.roll(); // Regenerate secret
|
||||
await token.revoke(); // Delete
|
||||
```
|
||||
|
||||
### 🔐 Certificates
|
||||
|
||||
```typescript
|
||||
const { certificates, summary } = await client.certificates.list();
|
||||
console.log(`${summary.valid} valid, ${summary.expiring} expiring, ${summary.failed} failed`);
|
||||
|
||||
// Operate on individual certificates
|
||||
const cert = certificates[0];
|
||||
await cert.reprovision();
|
||||
const exported = await cert.export();
|
||||
await cert.delete();
|
||||
|
||||
// Import a certificate
|
||||
await client.certificates.import({
|
||||
id: 'cert-id',
|
||||
domainName: 'example.com',
|
||||
created: Date.now(),
|
||||
validUntil: Date.now() + 90 * 24 * 3600 * 1000,
|
||||
privateKey: '...',
|
||||
publicKey: '...',
|
||||
csr: '...',
|
||||
});
|
||||
```
|
||||
|
||||
### 🌍 Remote Ingress
|
||||
|
||||
```typescript
|
||||
// List edges and their statuses
|
||||
const edges = await client.remoteIngress.list();
|
||||
const statuses = await client.remoteIngress.getStatuses();
|
||||
|
||||
// Create with builder
|
||||
const edge = await client.remoteIngress.build()
|
||||
.setName('edge-nyc-01')
|
||||
.setListenPorts([80, 443])
|
||||
.setAutoDerivePorts(true)
|
||||
.setTags(['us-east'])
|
||||
.save();
|
||||
|
||||
// Manage an edge
|
||||
await edge.update({ name: 'edge-nyc-02' });
|
||||
const newSecret = await edge.regenerateSecret();
|
||||
const token = await edge.getConnectionToken();
|
||||
await edge.delete();
|
||||
```
|
||||
|
||||
### 📊 Statistics (Read-Only)
|
||||
|
||||
```typescript
|
||||
const serverStats = await client.stats.getServer({ timeRange: '24h', includeHistory: true });
|
||||
const emailStats = await client.stats.getEmail({ domain: 'example.com' });
|
||||
const dnsStats = await client.stats.getDns();
|
||||
const security = await client.stats.getSecurity({ includeDetails: true });
|
||||
const connections = await client.stats.getConnections({ protocol: 'https' });
|
||||
const queues = await client.stats.getQueues();
|
||||
const health = await client.stats.getHealth(true);
|
||||
const network = await client.stats.getNetwork();
|
||||
const combined = await client.stats.getCombined({ server: true, email: true });
|
||||
```
|
||||
|
||||
### ⚙️ Configuration & Logs
|
||||
|
||||
```typescript
|
||||
// Read-only configuration
|
||||
const config = await client.config.get();
|
||||
const emailSection = await client.config.get('email');
|
||||
|
||||
// Logs
|
||||
const { logs, total, hasMore } = await client.logs.getRecent({
|
||||
level: 'error',
|
||||
category: 'smtp',
|
||||
limit: 50,
|
||||
});
|
||||
```
|
||||
|
||||
### 📧 Email Operations
|
||||
|
||||
```typescript
|
||||
const emails = await client.emails.list();
|
||||
const email = emails[0];
|
||||
const detail = await email.getDetail();
|
||||
await email.resend();
|
||||
|
||||
// Or use the manager directly
|
||||
const detail2 = await client.emails.getDetail('email-id');
|
||||
await client.emails.resend('email-id');
|
||||
```
|
||||
|
||||
### 📡 RADIUS
|
||||
|
||||
```typescript
|
||||
// Client management
|
||||
const clients = await client.radius.clients.list();
|
||||
await client.radius.clients.set({
|
||||
name: 'switch-1',
|
||||
ipRange: '192.168.1.0/24',
|
||||
secret: 'shared-secret',
|
||||
enabled: true,
|
||||
});
|
||||
await client.radius.clients.remove('switch-1');
|
||||
|
||||
// VLAN management
|
||||
const { mappings, config: vlanConfig } = await client.radius.vlans.list();
|
||||
await client.radius.vlans.set({ mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true });
|
||||
const result = await client.radius.vlans.testAssignment('aa:bb:cc:dd:ee:ff');
|
||||
await client.radius.vlans.updateConfig({ defaultVlan: 200 });
|
||||
|
||||
// Sessions
|
||||
const { sessions } = await client.radius.sessions.list({ vlanId: 10 });
|
||||
await client.radius.sessions.disconnect('session-id', 'Admin disconnect');
|
||||
|
||||
// Statistics & Accounting
|
||||
const stats = await client.radius.getStatistics();
|
||||
const summary = await client.radius.getAccountingSummary(startTime, endTime);
|
||||
```
|
||||
|
||||
## API Surface
|
||||
|
||||
| Manager | Methods |
|
||||
|---------|---------|
|
||||
| `client.login()` / `logout()` / `verifyIdentity()` | Authentication |
|
||||
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
|
||||
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
|
||||
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
|
||||
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
|
||||
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getRateLimits()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
|
||||
| `client.config` | `get(section?)` |
|
||||
| `client.logs` | `getRecent()`, `getStream()` |
|
||||
| `client.emails` | `list()`, `getDetail()`, `resend()` → Email: `getDetail()`, `resend()` |
|
||||
| `client.radius` | `.clients.list/set/remove()`, `.vlans.list/set/remove/updateConfig/testAssignment()`, `.sessions.list/disconnect()`, `getStatistics()`, `getAccountingSummary()` |
|
||||
|
||||
## Architecture
|
||||
|
||||
The client uses HTTP-based [TypedRequest](https://code.foss.global/api.global/typedrequest) for transport. All requests are sent as POST to `{baseUrl}/typedrequest`. Authentication (JWT identity and/or API token) is automatically injected into every request payload via `buildRequestPayload()`.
|
||||
|
||||
Resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) hold a reference to the client and provide instance methods that fire the appropriate TypedRequest operations. Builder classes (`RouteBuilder`, `ApiTokenBuilder`, `RemoteIngressBuilder`) use fluent chaining and a terminal `.save()` method.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
3
ts_apiclient/tspublish.json
Normal file
3
ts_apiclient/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 4
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export interface IRemoteIngress {
|
||||
name: string;
|
||||
secret: string;
|
||||
listenPorts: number[];
|
||||
/** UDP listen ports (e.g. for QUIC/HTTP3). Derived from routes with transport 'udp' or 'all'. */
|
||||
listenPortsUdp?: number[];
|
||||
enabled: boolean;
|
||||
/** Whether to auto-derive ports from remoteIngress-tagged routes. Defaults to true. */
|
||||
autoDerivePorts: boolean;
|
||||
@@ -20,6 +22,8 @@ export interface IRemoteIngress {
|
||||
manualPorts?: number[];
|
||||
/** Ports auto-derived from route configs — only present in API responses. */
|
||||
derivedPorts?: number[];
|
||||
/** Effective UDP ports (union of manual + derived) — only present in API responses. */
|
||||
effectiveListenPortsUdp?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -165,6 +165,7 @@ export interface INetworkMetrics {
|
||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond?: number;
|
||||
requestsTotal?: number;
|
||||
backends?: IBackendInfo[];
|
||||
}
|
||||
|
||||
export interface IConnectionDetails {
|
||||
@@ -174,4 +175,26 @@ export interface IConnectionDetails {
|
||||
startTime: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
}
|
||||
|
||||
export interface IBackendInfo {
|
||||
backend: string;
|
||||
domain: string | null;
|
||||
protocol: string;
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
connectErrors: number;
|
||||
handshakeErrors: number;
|
||||
requestErrors: number;
|
||||
avgConnectTimeMs: number;
|
||||
poolHitRate: number;
|
||||
h2Failures: number;
|
||||
h2Suppressed: boolean;
|
||||
h3Suppressed: boolean;
|
||||
h2CooldownRemainingSecs: number | null;
|
||||
h3CooldownRemainingSecs: number | null;
|
||||
h2ConsecutiveFailures: number | null;
|
||||
h3ConsecutiveFailures: number | null;
|
||||
h3Port: number | null;
|
||||
cacheAgeSecs: number | null;
|
||||
}
|
||||
@@ -82,6 +82,14 @@ interface IIdentity {
|
||||
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
||||
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
||||
|
||||
#### Route Management Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
| `IMergedRoute` | Combined route: routeConfig, source (hardcoded/programmatic), enabled, overridden |
|
||||
| `IRouteWarning` | Merge warning: disabled-hardcoded, disabled-programmatic, orphaned-override |
|
||||
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
|
||||
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
|
||||
|
||||
#### Remote Ingress Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
@@ -128,13 +136,29 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
||||
#### 📧 Email Operations
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetQueuedEmails` | `getQueuedEmails` | List queued emails |
|
||||
| `IReq_GetSentEmails` | `getSentEmails` | List delivered emails |
|
||||
| `IReq_GetFailedEmails` | `getFailedEmails` | List failed emails |
|
||||
| `IReq_GetAllEmails` | `getAllEmails` | List all emails |
|
||||
| `IReq_GetEmailDetail` | `getEmailDetail` | Full detail for a specific email |
|
||||
| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email |
|
||||
| `IReq_GetSecurityIncidents` | `getSecurityIncidents` | Security events |
|
||||
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
|
||||
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
|
||||
|
||||
#### 🛣️ Route Management
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetMergedRoutes` | `getMergedRoutes` | List all routes (hardcoded + programmatic) |
|
||||
| `IReq_CreateRoute` | `createRoute` | Create a new programmatic route |
|
||||
| `IReq_UpdateRoute` | `updateRoute` | Update a programmatic route |
|
||||
| `IReq_DeleteRoute` | `deleteRoute` | Delete a programmatic route |
|
||||
| `IReq_ToggleRoute` | `toggleRoute` | Enable/disable a programmatic route |
|
||||
| `IReq_SetRouteOverride` | `setRouteOverride` | Override a hardcoded route |
|
||||
| `IReq_RemoveRouteOverride` | `removeRouteOverride` | Remove a route override |
|
||||
|
||||
#### 🔑 API Token Management
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_CreateApiToken` | `createApiToken` | Create a new API token |
|
||||
| `IReq_ListApiTokens` | `listApiTokens` | List all tokens |
|
||||
| `IReq_RevokeApiToken` | `revokeApiToken` | Revoke (delete) a token |
|
||||
| `IReq_RollApiToken` | `rollApiToken` | Regenerate token secret |
|
||||
| `IReq_ToggleApiToken` | `toggleApiToken` | Enable/disable a token |
|
||||
|
||||
#### 🔐 Certificates
|
||||
| Interface | Method | Description |
|
||||
@@ -198,6 +222,8 @@ interface ICertificateInfo {
|
||||
|
||||
## Example: Full API Integration
|
||||
|
||||
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.
|
||||
|
||||
```typescript
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
||||
@@ -254,7 +280,7 @@ console.log('Connection token:', tokenResponse.token);
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
@@ -266,7 +292,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
@@ -179,5 +179,6 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp
|
||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||
requestsPerSecond: number;
|
||||
requestsTotal: number;
|
||||
backends?: statsInterfaces.IBackendInfo[];
|
||||
};
|
||||
}
|
||||
100
ts_oci_container/index.ts
Normal file
100
ts_oci_container/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { IDcRouterOptions } from '../ts/classes.dcrouter.js';
|
||||
|
||||
/**
|
||||
* Parses a comma-separated env var into a string array.
|
||||
* Returns undefined if the env var is not set or empty.
|
||||
*/
|
||||
function parseCommaSeparated(envVar: string | undefined): string[] | undefined {
|
||||
if (!envVar || envVar.trim() === '') return undefined;
|
||||
return envVar.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated env var into a number array.
|
||||
* Returns undefined if the env var is not set or empty.
|
||||
*/
|
||||
function parseCommaSeparatedNumbers(envVar: string | undefined): number[] | undefined {
|
||||
const parts = parseCommaSeparated(envVar);
|
||||
if (!parts) return undefined;
|
||||
return parts.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds IDcRouterOptions from environment variables for OCI container mode.
|
||||
*
|
||||
* If DCROUTER_CONFIG_PATH is set and the file exists, it is loaded as a JSON base config.
|
||||
* Individual env vars are then applied as overrides on top.
|
||||
*/
|
||||
export function getOciContainerConfig(): IDcRouterOptions {
|
||||
let options: IDcRouterOptions = {};
|
||||
|
||||
// Load JSON config file if specified
|
||||
const configPath = process.env.DCROUTER_CONFIG_PATH;
|
||||
if (configPath && plugins.fs.existsSync(configPath)) {
|
||||
const raw = plugins.fs.readFileSync(configPath, 'utf8');
|
||||
options = JSON.parse(raw);
|
||||
console.log(`[OCI Container] Loaded config from ${configPath}`);
|
||||
}
|
||||
|
||||
// Apply env var overrides
|
||||
if (process.env.DCROUTER_BASE_DIR) {
|
||||
options.baseDir = process.env.DCROUTER_BASE_DIR;
|
||||
}
|
||||
|
||||
// TLS config
|
||||
const tlsEmail = process.env.DCROUTER_TLS_EMAIL;
|
||||
const tlsDomain = process.env.DCROUTER_TLS_DOMAIN;
|
||||
if (tlsEmail || tlsDomain) {
|
||||
options.tls = {
|
||||
...options.tls,
|
||||
contactEmail: tlsEmail || options.tls?.contactEmail || '',
|
||||
...(tlsDomain ? { domain: tlsDomain } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Network config
|
||||
if (process.env.DCROUTER_PUBLIC_IP) {
|
||||
options.publicIp = process.env.DCROUTER_PUBLIC_IP;
|
||||
}
|
||||
|
||||
const proxyIps = parseCommaSeparated(process.env.DCROUTER_PROXY_IPS);
|
||||
if (proxyIps) {
|
||||
options.proxyIps = proxyIps;
|
||||
}
|
||||
|
||||
// DNS config
|
||||
const nsDomains = parseCommaSeparated(process.env.DCROUTER_DNS_NS_DOMAINS);
|
||||
if (nsDomains) {
|
||||
options.dnsNsDomains = nsDomains;
|
||||
}
|
||||
|
||||
const dnsScopes = parseCommaSeparated(process.env.DCROUTER_DNS_SCOPES);
|
||||
if (dnsScopes) {
|
||||
options.dnsScopes = dnsScopes;
|
||||
}
|
||||
|
||||
// Email config
|
||||
const emailHostname = process.env.DCROUTER_EMAIL_HOSTNAME;
|
||||
const emailPorts = parseCommaSeparatedNumbers(process.env.DCROUTER_EMAIL_PORTS);
|
||||
if (emailHostname || emailPorts) {
|
||||
options.emailConfig = {
|
||||
...options.emailConfig,
|
||||
...(emailHostname ? { hostname: emailHostname } : {}),
|
||||
...(emailPorts ? { ports: emailPorts } : {}),
|
||||
domains: options.emailConfig?.domains || [],
|
||||
routes: options.emailConfig?.routes || [],
|
||||
} as IDcRouterOptions['emailConfig'];
|
||||
}
|
||||
|
||||
// Cache config
|
||||
const cacheEnabled = process.env.DCROUTER_CACHE_ENABLED;
|
||||
if (cacheEnabled !== undefined) {
|
||||
options.cacheConfig = {
|
||||
...options.cacheConfig,
|
||||
enabled: cacheEnabled === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
7
ts_oci_container/plugins.ts
Normal file
7
ts_oci_container/plugins.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export {
|
||||
fs,
|
||||
path,
|
||||
};
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.0.38',
|
||||
version: '11.10.6',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface INetworkState {
|
||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond: number;
|
||||
requestsTotal: number;
|
||||
backends: interfaces.data.IBackendInfo[];
|
||||
lastUpdated: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
@@ -148,6 +149,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
throughputHistory: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
backends: [],
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -238,7 +240,7 @@ interface IActionContext {
|
||||
}
|
||||
|
||||
const getActionContext = (): IActionContext => {
|
||||
const identity = loginStatePart.getState().identity;
|
||||
const identity = loginStatePart.getState()!.identity;
|
||||
// Treat expired JWTs as no identity — prevents stale persisted sessions from firing requests
|
||||
if (identity && identity.expiresAt && identity.expiresAt < Date.now()) {
|
||||
return { identity: null };
|
||||
@@ -250,7 +252,7 @@ const getActionContext = (): IActionContext => {
|
||||
export const loginAction = loginStatePart.createAction<{
|
||||
username: string;
|
||||
password: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
}>(async (statePartArg, dataArg): Promise<ILoginState> => {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
||||
>('/typedrequest', 'adminLoginWithUsernameAndPassword');
|
||||
@@ -267,10 +269,10 @@ export const loginAction = loginStatePart.createAction<{
|
||||
isLoggedIn: true,
|
||||
};
|
||||
}
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return statePartArg.getState()!;
|
||||
} catch (error: unknown) {
|
||||
console.error('Login failed:', error);
|
||||
return statePartArg.getState();
|
||||
return statePartArg.getState()!;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -298,9 +300,9 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
|
||||
});
|
||||
|
||||
// Fetch All Stats Action - Using combined endpoint for efficiency
|
||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg): Promise<IStatsState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -308,7 +310,7 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
|
||||
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCombinedMetrics
|
||||
>('/typedrequest', 'getCombinedMetrics');
|
||||
|
||||
|
||||
const combinedResponse = await combinedRequest.fire({
|
||||
identity: context.identity,
|
||||
sections: {
|
||||
@@ -330,19 +332,19 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to fetch statistics',
|
||||
error: (error as Error).message || 'Failed to fetch statistics',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Configuration Action (read-only)
|
||||
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg): Promise<IConfigState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -359,11 +361,11 @@ export const fetchConfigurationAction = configStatePart.createAction(async (stat
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error.message || 'Failed to fetch configuration',
|
||||
error: (error as Error).message || 'Failed to fetch configuration',
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -373,9 +375,9 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
|
||||
limit?: number;
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
}>(async (statePartArg, dataArg): Promise<ILogState> => {
|
||||
const context = getActionContext();
|
||||
if (!context.identity) return statePartArg.getState();
|
||||
if (!context.identity) return statePartArg.getState()!;
|
||||
|
||||
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRecentLogs
|
||||
@@ -389,14 +391,14 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
|
||||
});
|
||||
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
...statePartArg.getState()!,
|
||||
recentLogs: response.logs,
|
||||
};
|
||||
});
|
||||
|
||||
// Toggle Auto Refresh Action
|
||||
export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg) => {
|
||||
const currentState = statePartArg.getState();
|
||||
export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg): Promise<IUiState> => {
|
||||
const currentState = statePartArg.getState()!;
|
||||
return {
|
||||
...currentState,
|
||||
autoRefresh: !currentState.autoRefresh,
|
||||
@@ -404,9 +406,9 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
|
||||
});
|
||||
|
||||
// Set Active View Action
|
||||
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName): Promise<IUiState> => {
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
// If switching to network view, ensure we fetch network data
|
||||
if (viewName === 'network' && currentState.activeView !== 'network') {
|
||||
setTimeout(() => {
|
||||
@@ -449,9 +451,9 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
||||
});
|
||||
|
||||
// Fetch Network Stats Action
|
||||
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -503,6 +505,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||
backends: networkStatsResponse.backends || [],
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -522,9 +525,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
// ============================================================================
|
||||
|
||||
// Fetch All Emails Action
|
||||
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg): Promise<IEmailOpsState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -555,9 +558,9 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
||||
// Certificate Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg): Promise<ICertificateState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -586,9 +589,9 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
||||
});
|
||||
|
||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||
async (statePartArg, domain, actionContext) => {
|
||||
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -596,13 +599,13 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
||||
>('/typedrequest', 'reprovisionCertificateDomain');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
domain,
|
||||
});
|
||||
|
||||
// Re-fetch overview after reprovisioning
|
||||
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
|
||||
@@ -612,9 +615,9 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
||||
);
|
||||
|
||||
export const deleteCertificateAction = certificateStatePart.createAction<string>(
|
||||
async (statePartArg, domain, actionContext) => {
|
||||
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -622,13 +625,13 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
|
||||
>('/typedrequest', 'deleteCertificate');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
domain,
|
||||
});
|
||||
|
||||
// Re-fetch overview after deletion
|
||||
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete certificate',
|
||||
@@ -646,9 +649,9 @@ export const importCertificateAction = certificateStatePart.createAction<{
|
||||
publicKey: string;
|
||||
csr: string;
|
||||
}>(
|
||||
async (statePartArg, cert, actionContext) => {
|
||||
async (statePartArg, cert, actionContext): Promise<ICertificateState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -656,13 +659,13 @@ export const importCertificateAction = certificateStatePart.createAction<{
|
||||
>('/typedrequest', 'importCertificate');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
cert,
|
||||
});
|
||||
|
||||
// Re-fetch overview after import
|
||||
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to import certificate',
|
||||
@@ -678,7 +681,7 @@ export async function fetchCertificateExport(domain: string) {
|
||||
>('/typedrequest', 'exportCertificate');
|
||||
|
||||
return request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
domain,
|
||||
});
|
||||
}
|
||||
@@ -692,16 +695,16 @@ export async function fetchConnectionToken(edgeId: string) {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRemoteIngressConnectionToken
|
||||
>('/typedrequest', 'getRemoteIngressConnectionToken');
|
||||
return request.fire({ identity: context.identity, edgeId });
|
||||
return request.fire({ identity: context.identity!, edgeId });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -740,9 +743,9 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -750,7 +753,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
>('/typedrequest', 'createRemoteIngress');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
autoDerivePorts: dataArg.autoDerivePorts,
|
||||
@@ -759,16 +762,16 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
|
||||
if (response.success) {
|
||||
// Refresh the list
|
||||
await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||
await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
||||
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
...statePartArg.getState()!,
|
||||
newEdgeId: response.edge.id,
|
||||
};
|
||||
}
|
||||
|
||||
return currentState;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to create edge',
|
||||
@@ -777,9 +780,9 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
});
|
||||
|
||||
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId, actionContext) => {
|
||||
async (statePartArg, edgeId, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -787,12 +790,12 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
|
||||
>('/typedrequest', 'deleteRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: edgeId,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete edge',
|
||||
@@ -807,9 +810,9 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -817,7 +820,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
>('/typedrequest', 'updateRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
@@ -825,8 +828,8 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to update edge',
|
||||
@@ -835,9 +838,9 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
});
|
||||
|
||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId) => {
|
||||
async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -845,7 +848,7 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
|
||||
>('/typedrequest', 'regenerateRemoteIngressSecret');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: edgeId,
|
||||
});
|
||||
|
||||
@@ -867,9 +870,9 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
|
||||
);
|
||||
|
||||
export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
|
||||
async (statePartArg) => {
|
||||
async (statePartArg): Promise<IRemoteIngressState> => {
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
...statePartArg.getState()!,
|
||||
newEdgeId: null,
|
||||
};
|
||||
}
|
||||
@@ -878,9 +881,9 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
|
||||
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -888,13 +891,13 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
>('/typedrequest', 'updateRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to toggle edge',
|
||||
@@ -906,9 +909,9 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
// Route Management Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -940,9 +943,9 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
|
||||
export const createRouteAction = routeManagementStatePart.createAction<{
|
||||
route: any;
|
||||
enabled?: boolean;
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -950,13 +953,13 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
||||
>('/typedrequest', 'createRoute');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
route: dataArg.route,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to create route',
|
||||
@@ -965,9 +968,9 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
||||
});
|
||||
|
||||
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, routeId, actionContext) => {
|
||||
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -975,12 +978,12 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||
>('/typedrequest', 'deleteRoute');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: routeId,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete route',
|
||||
@@ -992,9 +995,9 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||
export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -1002,13 +1005,13 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||
>('/typedrequest', 'toggleRoute');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to toggle route',
|
||||
@@ -1019,9 +1022,9 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||
routeName: string;
|
||||
enabled: boolean;
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -1029,13 +1032,13 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||
>('/typedrequest', 'setRouteOverride');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
routeName: dataArg.routeName,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to set override',
|
||||
@@ -1044,9 +1047,9 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||
});
|
||||
|
||||
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, routeName, actionContext) => {
|
||||
async (statePartArg, routeName, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -1054,12 +1057,12 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
|
||||
>('/typedrequest', 'removeRouteOverride');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
routeName,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to remove override',
|
||||
@@ -1072,9 +1075,9 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
|
||||
// API Token Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
if (!context.identity) return currentState;
|
||||
|
||||
try {
|
||||
@@ -1105,7 +1108,7 @@ export async function createApiToken(name: string, scopes: interfaces.data.TApiT
|
||||
>('/typedrequest', 'createApiToken');
|
||||
|
||||
return request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
name,
|
||||
scopes,
|
||||
expiresInDays,
|
||||
@@ -1119,15 +1122,15 @@ export async function rollApiToken(id: string) {
|
||||
>('/typedrequest', 'rollApiToken');
|
||||
|
||||
return request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, tokenId, actionContext) => {
|
||||
async (statePartArg, tokenId, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -1135,12 +1138,12 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
|
||||
>('/typedrequest', 'revokeApiToken');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: tokenId,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchApiTokensAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchApiTokensAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to revoke token',
|
||||
@@ -1152,9 +1155,9 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
|
||||
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}>(async (statePartArg, dataArg, actionContext) => {
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
@@ -1162,13 +1165,13 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
||||
>('/typedrequest', 'toggleApiToken');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
return await actionContext.dispatch(fetchApiTokensAction, null);
|
||||
} catch (error) {
|
||||
return await actionContext!.dispatch(fetchApiTokensAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to toggle token',
|
||||
@@ -1188,13 +1191,13 @@ socketRouter.addTypedHandler(
|
||||
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
||||
'pushLogEntry',
|
||||
async (dataArg) => {
|
||||
const current = logStatePart.getState();
|
||||
const current = logStatePart.getState()!;
|
||||
const updated = [...current.recentLogs, dataArg.entry];
|
||||
// Cap at 2000 entries
|
||||
if (updated.length > 2000) {
|
||||
updated.splice(0, updated.length - 2000);
|
||||
}
|
||||
logStatePart.setState({ ...current, recentLogs: updated });
|
||||
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
||||
return {};
|
||||
}
|
||||
)
|
||||
@@ -1229,14 +1232,14 @@ async function disconnectSocket() {
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
if (!context.identity) return;
|
||||
const currentView = uiStatePart.getState().activeView;
|
||||
const currentView = uiStatePart.getState()!.activeView;
|
||||
|
||||
try {
|
||||
// Always fetch basic stats for dashboard widgets
|
||||
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCombinedMetrics
|
||||
>('/typedrequest', 'getCombinedMetrics');
|
||||
|
||||
|
||||
const combinedResponse = await combinedRequest.fire({
|
||||
identity: context.identity,
|
||||
sections: {
|
||||
@@ -1249,12 +1252,13 @@ async function dispatchCombinedRefreshAction() {
|
||||
});
|
||||
|
||||
// Update all stats from combined response
|
||||
const currentStatsState = statsStatePart.getState()!;
|
||||
statsStatePart.setState({
|
||||
...statsStatePart.getState(),
|
||||
serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats,
|
||||
emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats,
|
||||
dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats,
|
||||
securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics,
|
||||
...currentStatsState,
|
||||
serverStats: combinedResponse.metrics.server || currentStatsState.serverStats,
|
||||
emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
|
||||
dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
|
||||
securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -1281,7 +1285,7 @@ async function dispatchCombinedRefreshAction() {
|
||||
});
|
||||
|
||||
networkStatePart.setState({
|
||||
...networkStatePart.getState(),
|
||||
...networkStatePart.getState()!,
|
||||
connections: connectionsResponse.connections,
|
||||
connectionsByIP,
|
||||
throughputRate: {
|
||||
@@ -1294,14 +1298,15 @@ async function dispatchCombinedRefreshAction() {
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
backends: network.backends || [],
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch connections:', error);
|
||||
networkStatePart.setState({
|
||||
...networkStatePart.getState(),
|
||||
...networkStatePart.getState()!,
|
||||
connections: [],
|
||||
connectionsByIP,
|
||||
throughputRate: {
|
||||
@@ -1314,6 +1319,7 @@ async function dispatchCombinedRefreshAction() {
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
backends: network.backends || [],
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -1356,9 +1362,9 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
||||
// Initialize auto-refresh when UI state is ready
|
||||
(() => {
|
||||
const startAutoRefresh = () => {
|
||||
const uiState = uiStatePart.getState();
|
||||
const loginState = loginStatePart.getState();
|
||||
|
||||
const uiState = uiStatePart.getState()!;
|
||||
const loginState = loginStatePart.getState()!;
|
||||
|
||||
// Only start if conditions are met and not already running at the same rate
|
||||
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||
// Check if we need to restart the interval (rate changed or not running)
|
||||
@@ -1384,9 +1390,9 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
||||
};
|
||||
|
||||
// Watch for relevant changes only
|
||||
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
|
||||
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
|
||||
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
|
||||
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
|
||||
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
|
||||
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
|
||||
|
||||
uiStatePart.state.subscribe((state) => {
|
||||
// Only restart if relevant values changed
|
||||
@@ -1417,7 +1423,7 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
||||
startAutoRefresh();
|
||||
|
||||
// Connect TypedSocket if already logged in (e.g., persistent session)
|
||||
if (loginStatePart.getState().isLoggedIn) {
|
||||
if (loginStatePart.getState()!.isLoggedIn) {
|
||||
connectSocket();
|
||||
}
|
||||
})();
|
||||
@@ -195,17 +195,18 @@ export class OpsDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
||||
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||
simpleLogin.addEventListener('login', (e: Event) => {
|
||||
// Handle logout event
|
||||
this.login(e.detail.data.username, e.detail.data.password);
|
||||
const detail = (e as CustomEvent).detail;
|
||||
this.login(detail.data.username, detail.data.password);
|
||||
});
|
||||
|
||||
// Handle view changes
|
||||
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
|
||||
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
|
||||
if (appDash) {
|
||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||
const viewName = e.detail.view.name.toLowerCase();
|
||||
appDash.addEventListener('view-select', (e: Event) => {
|
||||
const viewName = (e as CustomEvent).detail.view.name.toLowerCase();
|
||||
// Use router for navigation instead of direct state update
|
||||
appRouter.navigateToView(viewName);
|
||||
});
|
||||
@@ -217,7 +218,7 @@ export class OpsDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
// Handle initial state - check if we have a stored session that's still valid
|
||||
const loginState = appstate.loginStatePart.getState();
|
||||
const loginState = appstate.loginStatePart.getState()!;
|
||||
if (loginState.identity?.jwt) {
|
||||
if (loginState.identity.expiresAt > Date.now()) {
|
||||
// Client-side expiry looks valid — verify with server (keypair may have changed)
|
||||
@@ -229,7 +230,7 @@ export class OpsDashboard extends DeesElement {
|
||||
if (response.valid) {
|
||||
// JWT confirmed valid by server
|
||||
this.loginState = loginState;
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
await (simpleLogin as any).switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
} else {
|
||||
@@ -250,8 +251,8 @@ export class OpsDashboard extends DeesElement {
|
||||
private async login(username: string, password: string) {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
console.log(`Attempting to login...`);
|
||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
||||
const form = simpleLogin.shadowRoot.querySelector('dees-form');
|
||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||
const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
|
||||
form.setStatus('pending', 'Logging in...');
|
||||
|
||||
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||
@@ -262,14 +263,14 @@ export class OpsDashboard extends DeesElement {
|
||||
if (state.identity) {
|
||||
console.log('Login successful');
|
||||
this.loginState = state;
|
||||
form.setStatus('success', 'Logged in!');
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
form!.setStatus('success', 'Logged in!');
|
||||
await simpleLogin!.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
} else {
|
||||
form.setStatus('error', 'Login failed!');
|
||||
form!.setStatus('error', 'Login failed!');
|
||||
await domtools.convenience.smartdelay.delayFor(2000);
|
||||
form.reset();
|
||||
form!.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ declare global {
|
||||
@customElement('ops-view-certificates')
|
||||
export class OpsViewCertificates extends DeesElement {
|
||||
@state()
|
||||
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState();
|
||||
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -264,10 +264,10 @@ export class OpsViewCertificates extends DeesElement {
|
||||
{
|
||||
name: 'Import',
|
||||
iconName: 'lucide:upload',
|
||||
action: async (modal) => {
|
||||
action: async (modal: any) => {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
try {
|
||||
const form = modal.shadowRoot.querySelector('dees-form') as any;
|
||||
const form = modal.shadowRoot!.querySelector('dees-form') as any;
|
||||
const formData = await form.collectFormData();
|
||||
const files = formData.certJsonFile;
|
||||
if (!files || files.length === 0) {
|
||||
@@ -287,8 +287,8 @@ export class OpsViewCertificates extends DeesElement {
|
||||
);
|
||||
DeesToast.show({ message: `Certificate imported for ${cert.domainName}`, type: 'success', duration: 3000 });
|
||||
modal.destroy();
|
||||
} catch (err) {
|
||||
DeesToast.show({ message: `Import failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: `Import failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -339,8 +339,8 @@ export class OpsViewCertificates extends DeesElement {
|
||||
} else {
|
||||
DeesToast.show({ message: response.message || 'Export failed', type: 'error', duration: 4000 });
|
||||
}
|
||||
} catch (err) {
|
||||
DeesToast.show({ message: `Export failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: `Export failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -363,7 +363,7 @@ export class OpsViewCertificates extends DeesElement {
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash-2',
|
||||
action: async (modal) => {
|
||||
action: async (modal: any) => {
|
||||
try {
|
||||
await appstate.certificateStatePart.dispatchAction(
|
||||
appstate.deleteCertificateAction,
|
||||
@@ -371,8 +371,8 @@ export class OpsViewCertificates extends DeesElement {
|
||||
);
|
||||
DeesToast.show({ message: `Certificate deleted for ${cert.domain}`, type: 'success', duration: 3000 });
|
||||
modal.destroy();
|
||||
} catch (err) {
|
||||
DeesToast.show({ message: `Delete failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: `Delete failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
|
||||
private renderSystemSection(sys: NonNullable<appstate.IConfigState['config']>['system']): TemplateResult {
|
||||
// Annotate proxy IPs with source hint when Remote Ingress is active
|
||||
const ri = this.configState.config?.remoteIngress;
|
||||
let proxyIpValues: string[] | null = sys.proxyIps.length > 0 ? [...sys.proxyIps] : null;
|
||||
@@ -133,7 +133,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
|
||||
private renderSmartProxySection(proxy: NonNullable<appstate.IConfigState['config']>['smartProxy']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Route Count', value: proxy.routeCount },
|
||||
];
|
||||
@@ -164,7 +164,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
|
||||
private renderEmailSection(email: NonNullable<appstate.IConfigState['config']>['email']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
|
||||
{ key: 'Hostname', value: email.hostname },
|
||||
@@ -196,7 +196,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
|
||||
private renderDnsSection(dns: NonNullable<appstate.IConfigState['config']>['dns']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Port', value: dns.port },
|
||||
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
|
||||
@@ -216,7 +216,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
|
||||
private renderTlsSection(tls: NonNullable<appstate.IConfigState['config']>['tls']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Contact Email', value: tls.contactEmail },
|
||||
{ key: 'Domain', value: tls.domain },
|
||||
@@ -242,7 +242,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
|
||||
private renderCacheSection(cache: NonNullable<appstate.IConfigState['config']>['cache']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Storage Path', value: cache.storagePath },
|
||||
{ key: 'DB Name', value: cache.dbName },
|
||||
@@ -267,7 +267,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
|
||||
private renderRadiusSection(radius: NonNullable<appstate.IConfigState['config']>['radius']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Auth Port', value: radius.authPort },
|
||||
{ key: 'Accounting Port', value: radius.acctPort },
|
||||
@@ -296,7 +296,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
|
||||
private renderRemoteIngressSection(ri: NonNullable<appstate.IConfigState['config']>['remoteIngress']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Tunnel Port', value: ri.tunnelPort },
|
||||
{ key: 'Hub Domain', value: ri.hubDomain },
|
||||
|
||||
@@ -83,13 +83,13 @@ export class OpsViewEmails extends DeesElement {
|
||||
private async handleEmailClick(e: CustomEvent<interfaces.requests.IEmail>) {
|
||||
const emailSummary = e.detail;
|
||||
try {
|
||||
const context = appstate.loginStatePart.getState();
|
||||
const context = appstate.loginStatePart.getState()!;
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetEmailDetail
|
||||
>('/typedrequest', 'getEmailDetail');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
identity: context.identity!,
|
||||
emailId: emailSummary.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from './shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@@ -28,10 +29,10 @@ interface INetworkRequest {
|
||||
@customElement('ops-view-network')
|
||||
export class OpsViewNetwork extends DeesElement {
|
||||
@state()
|
||||
accessor statsState = appstate.statsStatePart.getState();
|
||||
accessor statsState = appstate.statsStatePart.getState()!;
|
||||
|
||||
@state()
|
||||
accessor networkState = appstate.networkStatePart.getState();
|
||||
accessor networkState = appstate.networkStatePart.getState()!;
|
||||
|
||||
|
||||
@state()
|
||||
@@ -198,6 +199,38 @@ export class OpsViewNetwork extends DeesElement {
|
||||
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
|
||||
}
|
||||
|
||||
.protocolBadge.h1 {
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')};
|
||||
color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')};
|
||||
}
|
||||
|
||||
.protocolBadge.h2 {
|
||||
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||
}
|
||||
|
||||
.protocolBadge.h3 {
|
||||
background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')};
|
||||
color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')};
|
||||
}
|
||||
|
||||
.protocolBadge.unknown {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||
color: ${cssManager.bdTheme('#757575', '#999999')};
|
||||
}
|
||||
|
||||
.suppressionBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
|
||||
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -265,6 +298,9 @@ export class OpsViewNetwork extends DeesElement {
|
||||
<!-- Top IPs Section -->
|
||||
${this.renderTopIPs()}
|
||||
|
||||
<!-- Backend Protocols Section -->
|
||||
${this.renderBackendProtocols()}
|
||||
|
||||
<!-- Requests Table -->
|
||||
<dees-table
|
||||
.data=${this.networkRequests}
|
||||
@@ -519,6 +555,106 @@ export class OpsViewNetwork extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBackendProtocols(): TemplateResult {
|
||||
const backends = this.networkState.backends;
|
||||
if (!backends || backends.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${backends}
|
||||
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
|
||||
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
|
||||
const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
return {
|
||||
'Backend': item.backend,
|
||||
'Domain': item.domain || '-',
|
||||
'Protocol': html`
|
||||
<span class="protocolBadge ${protocolClass}">${item.protocol.toUpperCase()}</span>
|
||||
${item.h2Suppressed ? html`<span class="suppressionBadge" title="H2 suppressed: ${item.h2ConsecutiveFailures ?? 0} failures, cooldown ${item.h2CooldownRemainingSecs ?? 0}s">H2 suppressed</span>` : ''}
|
||||
${item.h3Suppressed ? html`<span class="suppressionBadge" title="H3 suppressed: ${item.h3ConsecutiveFailures ?? 0} failures, cooldown ${item.h3CooldownRemainingSecs ?? 0}s">H3 suppressed</span>` : ''}
|
||||
`,
|
||||
'Active': item.activeConnections,
|
||||
'Total': this.formatNumber(item.totalConnections),
|
||||
'Avg Connect': item.avgConnectTimeMs > 0 ? `${item.avgConnectTimeMs.toFixed(1)}ms` : '-',
|
||||
'Pool Hit Rate': item.poolHitRate > 0 ? `${(item.poolHitRate * 100).toFixed(1)}%` : '-',
|
||||
'Errors': totalErrors > 0
|
||||
? html`<span class="statusBadge error">${totalErrors}</span>`
|
||||
: html`<span class="statusBadge success">0</span>`,
|
||||
'Cache Age': item.cacheAgeSecs != null ? `${Math.round(item.cacheAgeSecs)}s` : '-',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'lucide:info',
|
||||
type: ['inRow', 'doubleClick', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
await this.showBackendDetails(actionData.item);
|
||||
}
|
||||
}
|
||||
]}
|
||||
heading1="Backend Protocols"
|
||||
heading2="Auto-detected backend protocols and connection pool health"
|
||||
searchable
|
||||
.pagination=${false}
|
||||
dataName="backend"
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showBackendDetails(backend: interfaces.data.IBackendInfo) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Backend: ${backend.backend}`,
|
||||
content: html`
|
||||
<div style="padding: 20px;">
|
||||
<dees-dataview-codebox
|
||||
.heading=${'Backend Details'}
|
||||
progLang="json"
|
||||
.codeToDisplay=${JSON.stringify({
|
||||
backend: backend.backend,
|
||||
domain: backend.domain,
|
||||
protocol: backend.protocol,
|
||||
activeConnections: backend.activeConnections,
|
||||
totalConnections: backend.totalConnections,
|
||||
avgConnectTimeMs: backend.avgConnectTimeMs,
|
||||
poolHitRate: backend.poolHitRate,
|
||||
errors: {
|
||||
connect: backend.connectErrors,
|
||||
handshake: backend.handshakeErrors,
|
||||
request: backend.requestErrors,
|
||||
h2Failures: backend.h2Failures,
|
||||
},
|
||||
suppression: {
|
||||
h2Suppressed: backend.h2Suppressed,
|
||||
h3Suppressed: backend.h3Suppressed,
|
||||
h2CooldownRemainingSecs: backend.h2CooldownRemainingSecs,
|
||||
h3CooldownRemainingSecs: backend.h3CooldownRemainingSecs,
|
||||
h2ConsecutiveFailures: backend.h2ConsecutiveFailures,
|
||||
h3ConsecutiveFailures: backend.h3ConsecutiveFailures,
|
||||
},
|
||||
h3Port: backend.h3Port,
|
||||
cacheAgeSecs: backend.cacheAgeSecs,
|
||||
}, null, 2)}
|
||||
></dees-dataview-codebox>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Copy Backend Key',
|
||||
iconName: 'lucide:Copy',
|
||||
action: async () => {
|
||||
await navigator.clipboard.writeText(backend.backend);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private async updateNetworkData() {
|
||||
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||
|
||||
@@ -21,7 +21,7 @@ declare global {
|
||||
@customElement('ops-view-remoteingress')
|
||||
export class OpsViewRemoteIngress extends DeesElement {
|
||||
@state()
|
||||
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
|
||||
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -184,7 +184,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
@click=${async () => {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
try {
|
||||
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId);
|
||||
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId!);
|
||||
if (response.success && response.token) {
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||
await navigator.clipboard.writeText(response.token);
|
||||
@@ -202,8 +202,8 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
} else {
|
||||
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
|
||||
}
|
||||
} catch (err) {
|
||||
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
|
||||
}
|
||||
}}
|
||||
>Copy Connection Token</dees-button>
|
||||
@@ -399,8 +399,8 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
} else {
|
||||
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
|
||||
}
|
||||
} catch (err) {
|
||||
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
|
||||
} catch (err: unknown) {
|
||||
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -55,6 +55,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- Filter by log level (error, warning, info, debug)
|
||||
- Search and time-range selection
|
||||
|
||||
### 🛣️ Route & API Token Management
|
||||
- Programmatic route CRUD with enable/disable and override controls
|
||||
- API token creation, revocation, and scope management
|
||||
- Routes tab and API Tokens tab in unified view
|
||||
|
||||
### ⚙️ Configuration
|
||||
- Read-only display of current system configuration
|
||||
- Status badges for boolean values (enabled/disabled)
|
||||
@@ -96,6 +101,7 @@ ts_web/
|
||||
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||
├── ops-view-remoteingress.ts # Remote ingress edge management
|
||||
├── ops-view-logs.ts # Log viewer
|
||||
├── ops-view-routes.ts # Route & API token management
|
||||
├── ops-view-config.ts # Configuration display
|
||||
├── ops-view-security.ts # Security dashboard
|
||||
└── shared/
|
||||
@@ -171,6 +177,7 @@ fetchConnectionToken(edgeId) // Get connection token (standalone function)
|
||||
/emails/security → Security incidents
|
||||
/certificates → Certificate management
|
||||
/remoteingress → Remote ingress edge management
|
||||
/routes → Route & API token management
|
||||
/logs → Log viewer
|
||||
/configuration → System configuration
|
||||
/security → Security dashboard
|
||||
@@ -230,7 +237,7 @@ export class OpsViewMyView extends DeesElement {
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
@@ -242,7 +249,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
@@ -71,12 +71,12 @@ class AppRouter {
|
||||
|
||||
private updateViewState(view: string): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.uiStatePart.getState();
|
||||
const currentState = appstate.uiStatePart.getState()!;
|
||||
if (currentState.activeView !== view) {
|
||||
appstate.uiStatePart.setState({
|
||||
...currentState,
|
||||
activeView: view,
|
||||
});
|
||||
} as appstate.IUiState);
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class AppRouter {
|
||||
}
|
||||
|
||||
public getCurrentView(): string {
|
||||
return appstate.uiStatePart.getState().activeView;
|
||||
return appstate.uiStatePart.getState()!.activeView;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
|
||||
Reference in New Issue
Block a user