Compare commits

..

4 Commits

Author SHA1 Message Date
6d4e30e8a9 v11.5.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-19 16:54:39 +00:00
0e308b692b fix(project): no changes to commit 2026-03-19 16:54:39 +00:00
9f74b6e063 v11.5.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-19 16:41:16 +00:00
1d0f47f256 feat(opsserver): add configurable OpsServer port and update related tests and documentation 2026-03-19 16:41:16 +00:00
15 changed files with 85 additions and 57 deletions

View File

@@ -1,5 +1,18 @@
# Changelog
## 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

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "11.4.0",
"version": "11.5.1",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -25,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"
},
@@ -40,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",
@@ -53,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.7.0",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.9.0",
"@serve.zone/remoteingress": "^4.13.0",
"@tsclass/tsclass": "^9.4.0",
"lru-cache": "^11.2.7",
"uuid": "^13.0.0"

50
pnpm-lock.yaml generated
View File

@@ -39,8 +39,8 @@ importers:
specifier: ^6.1.3
version: 6.1.3
'@push.rocks/smartacme':
specifier: ^9.1.3
version: 9.1.3(socks@2.8.7)
specifier: ^9.3.0
version: 9.3.0(socks@2.8.7)
'@push.rocks/smartdata':
specifier: ^7.1.0
version: 7.1.0(socks@2.8.7)
@@ -78,8 +78,8 @@ importers:
specifier: ^4.2.3
version: 4.2.3
'@push.rocks/smartproxy':
specifier: ^25.11.24
version: 25.11.24
specifier: ^25.14.1
version: 25.14.1
'@push.rocks/smartradius':
specifier: ^1.1.1
version: 1.1.1
@@ -96,14 +96,14 @@ importers:
specifier: ^3.0.9
version: 3.0.9
'@serve.zone/catalog':
specifier: ^2.7.0
version: 2.8.0(@tiptap/pm@2.27.2)
specifier: ^2.9.0
version: 2.9.0(@tiptap/pm@2.27.2)
'@serve.zone/interfaces':
specifier: ^5.3.0
version: 5.3.0
'@serve.zone/remoteingress':
specifier: ^4.9.0
version: 4.9.1
specifier: ^4.13.0
version: 4.13.0
'@tsclass/tsclass':
specifier: ^9.4.0
version: 9.5.0
@@ -124,8 +124,8 @@ importers:
specifier: ^2.0.1
version: 2.0.1
'@git.zone/tstest':
specifier: ^3.3.2
version: 3.4.0(socks@2.8.7)(typescript@5.9.3)
specifier: ^3.5.0
version: 3.5.0(socks@2.8.7)(typescript@5.9.3)
'@git.zone/tswatch':
specifier: ^3.3.0
version: 3.3.0(@tiptap/pm@2.27.2)
@@ -554,8 +554,8 @@ packages:
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
hasBin: true
'@git.zone/tstest@3.4.0':
resolution: {integrity: sha512-EpIrwlfU8BfhO3na6bb/2RcBB2Zi8rbQfoO+QLY+rc1nj/iHW+mgSeDnXuKQTATI0OQ0xsq3xB4qIZ0tKSYvYw==}
'@git.zone/tstest@3.5.0':
resolution: {integrity: sha512-ugIJzdVkbgqSSw08SZajE7TB01GIYjEAmIy67O5skhvOyszGifwzJdR+8dS1VbQGlUUWQZMGQ2IowllHbAZYJQ==}
hasBin: true
'@git.zone/tswatch@3.3.0':
@@ -1079,8 +1079,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartacme@9.1.3':
resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==}
'@push.rocks/smartacme@9.3.0':
resolution: {integrity: sha512-R6+fBNqlIy3fP2ECmOjBB65tl35w2+2vmSierO6oC9/5DW+khwjvFsT0+5WnfyjejEtWzdAprEseYWmBbyTGtA==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -1256,8 +1256,8 @@ packages:
'@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@25.11.24':
resolution: {integrity: sha512-dkeQJM2W5wKwTBZXxy3hGhIyQv2XeAQqwWliDbi/2oy6LU6BFdgpzEdhE5emG+wOcfQTagALpOIt7lBAzetjSA==}
'@push.rocks/smartproxy@25.14.1':
resolution: {integrity: sha512-QXJ1M7Or81lmCusAKkmIB8M9jJwl1/AKItnmTkn8IQ9zsPd6r+0uhP1j5tCO/LwRQRpzOADnpSrpVcrtMKK9kQ==}
'@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1544,14 +1544,14 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.8.0':
resolution: {integrity: sha512-p0ES14JwUoJE88DBtLSHcCfFPVa0vKhvHnQLaAY3OC15kfheNKidi1SwTFyMh43jj0ZNi4Lecc3W02wG6sasHw==}
'@serve.zone/catalog@2.9.0':
resolution: {integrity: sha512-7FgwS44pD/DFVj29jS0Kwwyn1i5h8cf4/yWMBEY8+8GO70ab3QctbcKMu+BVa1G3gIrpLqhpmxLFDoeL/zDnQA==}
'@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@4.9.1':
resolution: {integrity: sha512-z3UwPIlcrQp1fm1BpSY5JxUorDiBawd2h7St5FNWbL6X4qCWu+/fLB+EdAyZQy+JD02lRglZiaxk4wpnp8rQkA==}
'@serve.zone/remoteingress@4.13.0':
resolution: {integrity: sha512-Gw/yIgCukh3kImIco3u9B+b2cQP4l88RgCdP7NhYpwTDrI9jrsKmrzq0cRXo/Lnja35RZ1D7fBmNvaaAqEToVQ==}
'@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -5142,7 +5142,7 @@ snapshots:
'@push.rocks/smartshell': 3.3.8
tsx: 4.21.0
'@git.zone/tstest@3.4.0(socks@2.8.7)(typescript@5.9.3)':
'@git.zone/tstest@3.5.0(socks@2.8.7)(typescript@5.9.3)':
dependencies:
'@git.zone/tsbundle': 2.9.1
'@git.zone/tsrun': 2.0.1
@@ -5942,7 +5942,7 @@ snapshots:
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
'@push.rocks/smartacme@9.3.0(socks@2.8.7)':
dependencies:
'@apiclient.xyz/cloudflare': 7.1.0
'@peculiar/x509': 1.14.3
@@ -6539,7 +6539,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.11.24':
'@push.rocks/smartproxy@25.14.1':
dependencies:
'@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.1
@@ -6937,7 +6937,7 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@serve.zone/catalog@2.8.0(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.9.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.49.0(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.1
@@ -6956,7 +6956,7 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.5.0
'@serve.zone/remoteingress@4.9.1':
'@serve.zone/remoteingress@4.13.0':
dependencies:
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartrust': 1.3.2

View File

@@ -344,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.
@@ -425,6 +425,10 @@ interface IDcRouterOptions {
};
};
// ── OpsServer ────────────────────────────────────────────────
/** Port for the OpsServer web dashboard (default: 3000) */
opsServerPort?: number;
// ── TLS & Certificates ────────────────────────────────────────
tls?: {
contactEmail: string;
@@ -1016,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

View File

@@ -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,
}

View File

@@ -9,6 +9,7 @@ tap.test('should NOT instantiate DNS server when dnsNsDomains is not set', async
smartProxyConfig: {
routes: []
},
opsServerPort: 3100,
cacheConfig: { enabled: false }
});

View File

@@ -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'
);

View File

@@ -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'
);

View File

@@ -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'
);

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.4.0',
version: '11.5.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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

View 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);

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.4.0',
version: '11.5.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}