Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f74b6e063 | |||
| 1d0f47f256 | |||
| 4e9301ae2a | |||
| 7e2142ce53 | |||
| 67190605a6 | |||
| 9479a07ddf | |||
| fbed56092f | |||
| 547b82b35b | |||
| 3dc63fa02e | |||
| e0154f5b70 | |||
| b268409897 | |||
| f3a9fd12c5 |
@@ -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
|
||||
|
||||
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" ]
|
||||
|
||||
|
||||
38
changelog.md
38
changelog.md
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -72,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"]
|
||||
}
|
||||
}
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "11.2.53",
|
||||
"version": "11.5.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -16,6 +16,8 @@
|
||||
"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"
|
||||
},
|
||||
@@ -23,7 +25,7 @@
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tsbundle": "^2.9.1",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.3.2",
|
||||
"@git.zone/tstest": "^3.5.0",
|
||||
"@git.zone/tswatch": "^3.3.0",
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
@@ -38,7 +40,7 @@
|
||||
"@push.rocks/lik": "^6.3.1",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^9.1.3",
|
||||
"@push.rocks/smartacme": "^9.3.0",
|
||||
"@push.rocks/smartdata": "^7.1.0",
|
||||
"@push.rocks/smartdns": "^7.9.0",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
@@ -51,15 +53,15 @@
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^25.11.24",
|
||||
"@push.rocks/smartproxy": "^25.14.1",
|
||||
"@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/smartunique": "^3.0.9",
|
||||
"@serve.zone/catalog": "^2.6.2",
|
||||
"@serve.zone/catalog": "^2.9.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@serve.zone/remoteingress": "^4.8.14",
|
||||
"@serve.zone/remoteingress": "^4.13.0",
|
||||
"@tsclass/tsclass": "^9.4.0",
|
||||
"lru-cache": "^11.2.7",
|
||||
"uuid": "^13.0.0"
|
||||
|
||||
1811
pnpm-lock.yaml
generated
1811
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
54
readme.md
54
readme.md
@@ -30,6 +30,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [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
|
||||
@@ -343,7 +344,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.
|
||||
|
||||
@@ -424,6 +425,10 @@ interface IDcRouterOptions {
|
||||
};
|
||||
};
|
||||
|
||||
// ── OpsServer ────────────────────────────────────────────────
|
||||
/** Port for the OpsServer web dashboard (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
|
||||
// ── TLS & Certificates ────────────────────────────────────────
|
||||
tls?: {
|
||||
contactEmail: string;
|
||||
@@ -1015,7 +1020,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
|
||||
|
||||
@@ -1278,6 +1283,49 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
| `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.
|
||||
@@ -1292,7 +1340,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.
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
|
||||
@@ -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.2.53',
|
||||
version: '11.5.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -163,6 +163,9 @@ export interface IDcRouterOptions {
|
||||
* Remote Ingress configuration for edge tunnel nodes
|
||||
* Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
|
||||
*/
|
||||
/** Port for the OpsServer web UI (default: 3000) */
|
||||
opsServerPort?: number;
|
||||
|
||||
remoteIngressConfig?: {
|
||||
/** Enable remote ingress hub (default: false) */
|
||||
enabled?: boolean;
|
||||
|
||||
23
ts/index.ts
23
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,24 @@ export * from './radius/index.js';
|
||||
// Remote Ingress module
|
||||
export * from './remoteingress/index.js';
|
||||
|
||||
export const runCli = async () => {};
|
||||
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.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
@@ -71,7 +71,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -271,7 +271,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.
|
||||
|
||||
@@ -292,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.
|
||||
|
||||
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.2.53',
|
||||
version: '11.5.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -249,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.
|
||||
|
||||
Reference in New Issue
Block a user