From 39f449cbe4a1a92f783a53ca7d11a15d4938d202 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 15 Apr 2026 19:59:04 +0000 Subject: [PATCH] feat(routes,email): persist system DNS routes with runtime hydration and add reusable email ops DNS helpers --- changelog.md | 7 + package.json | 3 +- readme.md | 1799 ++--------------- test/test.dns-runtime-routes.node.ts | 146 +- test/test.email-dns-records.node.ts | 65 + test/test.email-ops-api.ts | 167 ++ test/test.email-ops-handlers.node.ts | 107 + ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 108 +- ts/config/classes.route-config-manager.ts | 139 +- ts/db/documents/classes.route.doc.ts | 7 + ts/email/classes.email-domain.manager.ts | 36 +- ts/email/email-dns-records.ts | 53 + ts/email/index.ts | 1 + .../handlers/route-management.handler.ts | 10 +- ts_apiclient/readme.md | 272 +-- ts_interfaces/data/route-management.ts | 2 + ts_interfaces/readme.md | 343 +--- ts_migrations/index.ts | 33 + ts_migrations/readme.md | 67 + ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 20 +- ts_web/elements/network/ops-view-routes.ts | 76 +- ts_web/readme.md | 281 +-- 24 files changed, 1221 insertions(+), 2525 deletions(-) create mode 100644 test/test.email-dns-records.node.ts create mode 100644 test/test.email-ops-api.ts create mode 100644 test/test.email-ops-handlers.node.ts create mode 100644 ts/email/email-dns-records.ts create mode 100644 ts_migrations/readme.md diff --git a/changelog.md b/changelog.md index 3f6ffde..4ebce0d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-15 - 13.19.0 - feat(routes,email) +persist system DNS routes with runtime hydration and add reusable email ops DNS helpers + +- Persist seeded DNS-over-HTTPS routes with stable system keys and hydrate socket handlers at runtime instead of treating them as runtime-only routes +- Restrict system-managed routes to toggle-only operations across the route manager, Ops API, and web UI while returning explicit mutation errors +- Add a shared email DNS record builder and cover email queue operations and handler behavior with new tests + ## 2026-04-14 - 13.18.0 - feat(email) add persistent smartmta storage and runtime-managed email domain syncing diff --git a/package.json b/package.json index 75c3894..301fe5c 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "@git.zone/tsrun": "^2.0.2", "@git.zone/tstest": "^3.6.3", "@git.zone/tswatch": "^3.3.2", - "@types/node": "^25.6.0", - "typescript": "^6.0.2" + "@types/node": "^25.6.0" }, "dependencies": { "@api.global/typedrequest": "^3.3.0", diff --git a/readme.md b/readme.md index b76fc8f..eb5f093 100644 --- a/readme.md +++ b/readme.md @@ -1,139 +1,181 @@ # @serve.zone/dcrouter -![](https://code.foss.global/serve.zone/docs/raw/branch/main/dcrouter.png) +![dcrouter banner](https://code.foss.global/serve.zone/docs/raw/branch/main/dcrouter.png) -**dcrouter: The all-in-one gateway for your datacenter.** πŸš€ +`dcrouter` is a TypeScript control plane for running a serious multi-protocol edge or datacenter gateway from one process. It orchestrates HTTP/HTTPS and TCP routing through SmartProxy, email through smartmta, authoritative DNS and DNS-over-HTTPS, RADIUS, remote ingress tunnels, VPN access control, a typed Ops API, and a web dashboard. -A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS, RADIUS, VPN, and remote edge ingress β€” all from a single process. Designed for enterprises requiring robust traffic management, automatic TLS certificate provisioning, VPN-based access control, distributed edge networking, and enterprise-grade email infrastructure. +It is built for operators who want one place to define routes, expose services, manage certificates, register domains and DNS providers, control VPN-only access, and inspect what is going on in production. ## 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. -## Table of Contents +## Why dcrouter -- [Features](#features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [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) -- [Remote Ingress](#remote-ingress) -- [VPN Access Control](#vpn-access-control) -- [Certificate Management](#certificate-management) -- [Storage & Database](#storage--database) -- [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) +- 🌐 Run HTTP/HTTPS, TCP/SNI, email, DNS, RADIUS, VPN, and remote ingress from one orchestrated service. +- πŸ” Keep certificates, routes, tokens, domains, and reusable route references in one management plane. +- 🧠 Use system-managed routes for config-, email-, and DNS-derived traffic, plus API-managed routes for dynamic additions. +- πŸ“Š Get an Ops UI and TypedRequest API for monitoring, automation, and day-2 operations. +- ⚑ Lean on Rust-backed data planes where it matters: proxying, DNS, email delivery, remote ingress, and VPN. -## Features +## What It Covers -### 🌐 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) - -### πŸ“§ Complete Email Infrastructure (powered by [smartmta](https://code.foss.global/push.rocks/smartmta)) -- **Multi-domain SMTP server** on standard ports (25, 587, 465) -- **Pattern-based email routing** with four action types: forward, process, deliver, reject -- **DKIM signing & verification**, SPF, DMARC authentication stack -- **Enterprise deliverability** with IP warmup schedules and sender reputation tracking -- **Bounce handling** with automatic suppression lists -- **Hierarchical rate limiting** β€” global, per-domain, per-sender - -### πŸ”’ Enterprise Security -- **Automatic TLS certificates** via ACME (smartacme v9) with Cloudflare DNS-01 challenges -- **Smart certificate scheduling** β€” per-domain deduplication, controlled parallelism, and account rate limiting handled automatically -- **Per-domain exponential backoff** β€” failed provisioning attempts are tracked and backed off to avoid hammering ACME servers -- **IP reputation checking** with caching and configurable thresholds -- **Content scanning** for spam, viruses, and malicious attachments -- **Security event logging** with structured audit trails - -### πŸ“‘ RADIUS Server -- **MAC Authentication Bypass (MAB)** for network device authentication -- **VLAN assignment** based on exact MAC, OUI prefix, or wildcard patterns -- **RADIUS accounting** for session tracking, traffic metering, and billing -- **Real-time management** via OpsServer API - -### 🌍 Remote Ingress (powered by [remoteingress](https://code.foss.global/serve.zone/remoteingress)) -- **Distributed edge networking** β€” accept traffic at remote edge nodes and tunnel it to the hub -- **Edge registration CRUD** with secret-based authentication -- **Auto-derived ports** β€” edges automatically pick up ports from routes tagged with `remoteIngress.enabled` -- **Connection tokens** β€” generate a single opaque base64url token containing hubHost, hubPort, edgeId, and secret for easy edge provisioning -- **Real-time status monitoring** β€” connected/disconnected state, public IP, active tunnels, heartbeat tracking -- **OpsServer dashboard** with enable/disable, edit, secret regeneration, token copy, and delete actions - -### πŸ” VPN Access Control (powered by [smartvpn](https://code.foss.global/push.rocks/smartvpn)) -- **WireGuard + native transports** β€” standard WireGuard clients (iOS, Android, macOS, Windows, Linux) plus custom WebSocket/QUIC tunnels -- **Route-level VPN gating** β€” mark any route with `vpn: { enabled: true }` to restrict access to VPN clients only, or `vpn: { enabled: true, mandatory: false }` to add VPN clients alongside existing access rules -- **Tag-based access control** β€” assign `serverDefinedClientTags` to clients and restrict routes with `allowedServerDefinedClientTags` -- **Constructor-defined clients** β€” pre-define VPN clients with tags in config for declarative, code-driven setup -- **Rootless operation** β€” uses userspace NAT (smoltcp) with no root required -- **Destination policy** β€” configurable `forceTarget`, `block`, or `allow` with allowList/blockList for granular traffic control -- **Client management** β€” create, enable, disable, rotate keys, export WireGuard/SmartVPN configs via OpsServer API and dashboard -- **IP-based enforcement** β€” VPN clients get IPs from a configurable subnet; SmartProxy enforces `ipAllowList` per route -- **PROXY protocol v2** β€” the NAT engine sends PP v2 on outbound connections to preserve VPN client identity - -### ⚑ High Performance -- **Rust-powered proxy engine** via SmartProxy for maximum throughput -- **Rust-powered MTA engine** via smartmta (TypeScript + Rust hybrid) for reliable email delivery -- **Rust-powered DNS engine** via SmartDNS for high-performance UDP and DNS-over-HTTPS -- **Connection pooling** for outbound SMTP and backend services -- **Socket-handler mode** β€” direct socket passing eliminates internal port hops -- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput) - -### πŸ’Ύ Unified Database -- **Two deployment modes**: embedded LocalSmartDb (zero-config) or external MongoDB -- **15 document classes** covering routes, certs, VPN, RADIUS, security profiles, network targets, and caches -- **Automatic TTL-based cleanup** for cached emails and IP reputation data -- **Reusable references** β€” security profiles and network targets that propagate changes to all referencing routes - -### πŸ–₯️ OpsServer Dashboard -- **Web-based management interface** with real-time monitoring -- **JWT authentication** with session persistence -- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, VPN clients, and security events -- **Domain-centric certificate overview** with backoff status and one-click reprovisioning -- **Remote ingress management** with connection token generation and one-click copy -- **Security profiles & network targets** β€” reusable security configurations and host:port targets with propagation to referencing routes -- **Global warning banners** when database is disabled (management features unavailable) -- **Read-only configuration display** for system overview -- **Smart tab visibility handling** β€” auto-pauses all polling, WebSocket connections, and chart updates when the browser tab is hidden, preventing resource waste and tab freezing - -### πŸ”§ 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 +| Area | What dcrouter does | +| --- | --- | +| HTTP / HTTPS / TCP | SmartProxy-based routing, TLS termination or passthrough, path/domain matching, optional HTTP/3 augmentation | +| Email | smartmta-based SMTP ingress and delivery, route-based email handling, DKIM-aware domain support | +| DNS | Authoritative DNS, DNS-over-HTTPS bootstrap routes, provider-backed and dcrouter-hosted domains and records | +| Certificates | ACME-aware certificate management with dashboard and API support | +| Access control | Source profiles, network targets, VPN-gated routes, API tokens, admin auth | +| Network edge | Remote ingress hub for edge nodes tunneling traffic into the router | +| Operations | Web dashboard, TypedRequest API, logs, metrics, health, route and token management | ## Installation ```bash pnpm add @serve.zone/dcrouter -# or -npm install @serve.zone/dcrouter ``` -### Prerequisites - -- **Node.js 20+** with ES module support -- Valid domain with DNS control (for ACME certificate automation) -- Cloudflare API token (for DNS-01 challenges) β€” optional - ## Quick Start -### Basic HTTP/HTTPS Router +This is the smallest realistic setup: one HTTP route, embedded database enabled, and the Ops dashboard on port `3000`. + +```typescript +import { DcRouter } from '@serve.zone/dcrouter'; + +const router = new DcRouter({ + smartProxyConfig: { + routes: [ + { + name: 'app', + match: { + domains: ['app.example.com'], + ports: [80], + }, + action: { + type: 'forward', + targets: [{ host: '127.0.0.1', port: 3001 }], + }, + }, + ], + }, + dbConfig: { + enabled: true, + }, + opsServerPort: 3000, +}); + +await router.start(); +``` + +Once the router is running, you can: + +- open the Ops dashboard on `http://localhost:3000` +- inspect the route in the System Routes view +- add API-managed routes through the dashboard or API client +- enable DNS, email, VPN, remote ingress, or RADIUS by adding the corresponding config blocks + +## Mental Model + +`dcrouter` is not a toy reverse proxy with a few side features. It is an orchestrator that wires multiple specialized services into one management plane. + +| Layer | Responsibility | +| --- | --- | +| `DcRouter` | Startup order, shutdown, service wiring, configuration assembly, route hydration | +| SmartProxy | HTTP/HTTPS, TCP/SNI, TLS, HTTP/3-capable route execution | +| smartmta | SMTP ingress, queueing, DKIM-aware email processing and delivery | +| SmartDNS | Authoritative DNS and DoH request handling | +| smartradius | Network authentication, VLAN assignment, accounting | +| remoteingress | Edge tunnel registrations and runtime forwarding into the hub | +| smartvpn | VPN server and client access mediation for protected routes | +| OpsServer + dashboard | Typed API and browser UI for operations | +| smartdata-backed DB | Persistent routes, tokens, domains, records, profiles, cert metadata, caches | + +## Route Model + +Routes fall into two ownership classes: + +| Route kind | Origin | Ownership | What users can do | +| --- | --- | --- | --- | +| System routes | `config`, `email`, `dns` | Derived from config or runtime-managed subsystems | View and toggle only | +| API routes | `api` | Created through route-management API | Create, edit, delete, toggle | + +Important details: + +- system routes are persisted with a stable `systemKey` +- config-, email-, and DNS-derived routes show up in the System Routes view +- DoH routes are persisted as system-route templates and get their live socket handlers attached at apply time +- system routes are managed by the system, not edited directly by operators + +## Core Features + +### Traffic Routing + +- Domain-, port-, and path-based SmartProxy routes +- HTTP/HTTPS reverse proxying and generic TCP/SNI forwarding +- Optional HTTP/3 augmentation for qualifying HTTPS routes +- Reusable source profiles and network targets for route composition +- Remote ingress aware routing for edge-delivered traffic + +### Email + +- smartmta-based inbound email handling +- Route-based mail actions such as forward, process, deliver, reject +- DKIM-aware domain handling and DNS record generation support +- Email-domain management through the Ops API and UI +- Queue, resend, failure, and delivery inspection through the dashboard and API + +### DNS + +- Authoritative scopes via `dnsScopes` +- Bootstrap nameserver domains via `dnsNsDomains` +- DNS-over-HTTPS endpoints for `/dns-query` and `/resolve` +- Managed domains, managed records, and provider-backed DNS integrations +- Internal email DNS record generation for `internal-dns` email domains + +### Certificates and ACME + +- Certificate overview and operations through OpsServer +- Import, export, delete, and reprovision flows +- DB-backed ACME configuration management +- Integration with managed DNS for certificate provisioning flows +- Routes can declare `certificate: 'auto'`, but actual automated issuance depends on ACME being configured in the management plane + +### VPN, RADIUS, and Remote Ingress + +- VPN-gated routes with target-profile-based access matching +- WireGuard-oriented VPN management with dcrouter-side client lifecycle support +- RADIUS MAB, VLAN assignment, and accounting +- Remote ingress hub for edge nodes tunneling traffic into central routes + +### Operations Plane + +- Web dashboard with overview, network, routes, access, security, domains, certificates, logs, and email views +- TypedRequest API for automation and external control +- API tokens with scoped access +- Metrics, health, logs, and per-feature operational views + +## Configuration Overview + +The main entry point is `IDcRouterOptions`. + +| Option | Purpose | +| --- | --- | +| `smartProxyConfig` | Main HTTP/HTTPS and TCP/SNI routing configuration | +| `emailConfig` | smartmta server config and email routes | +| `emailPortConfig` | External-to-internal email port mapping and email storage path tuning | +| `dnsNsDomains` | Nameserver hostnames used for NS bootstrap and DoH routes | +| `dnsScopes` | Authoritative DNS zones managed by dcrouter | +| `dnsRecords` | Static constructor-defined records | +| `publicIp` / `proxyIps` | DNS A-record exposure strategy | +| `dbConfig` | Embedded or external Mongo-backed persistence and seeding | +| `radiusConfig` | RADIUS authentication, VLAN, and accounting setup | +| `remoteIngressConfig` | Edge tunnel hub setup | +| `vpnConfig` | VPN server and client access configuration | +| `http3` | Global HTTP/3 behavior for qualifying routes | +| `opsServerPort` | Dashboard and TypedRequest API port | + +## Example: Enabling DNS, Email, and VPN ```typescript import { DcRouter } from '@serve.zone/dcrouter'; @@ -143,1532 +185,137 @@ const router = new DcRouter({ routes: [ { name: 'web-app', - match: { domains: ['example.com', 'www.example.com'], ports: [443] }, + match: { + domains: ['app.example.com'], + ports: [443], + }, action: { type: 'forward', - targets: [{ host: '192.168.1.10', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' } - } - } + targets: [{ host: '127.0.0.1', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + }, + }, ], - acme: { - email: 'admin@example.com', - enabled: true, - useProduction: true - } - } -}); - -await router.start(); -``` - -### Basic Email Server - -```typescript -import { DcRouter } from '@serve.zone/dcrouter'; - -const router = new DcRouter({ + }, emailConfig: { - ports: [25, 587, 465], hostname: 'mail.example.com', + ports: [25, 587, 465], domains: [ { domain: 'example.com', - dnsMode: 'external-dns' - } + dnsMode: 'internal-dns', + }, ], - routes: [ - { - name: 'process-all', - match: { recipients: '*@example.com' }, - action: { - type: 'process', - process: { scan: true, dkim: true, queue: 'normal' } - } - } - ] - } -}); - -await router.start(); -``` - -### Full Stack with Dashboard - -```typescript -import { DcRouter } from '@serve.zone/dcrouter'; - -const router = new DcRouter({ - // HTTP/HTTPS routing - smartProxyConfig: { - routes: [ - { - name: 'website', - match: { domains: ['example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.10', port: 80 }], - tls: { mode: 'terminate', certificate: 'auto' } - } - } - ], - acme: { email: 'ssl@example.com', enabled: true, useProduction: true } - }, - - // Email system (powered by smartmta) - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.example.com', - domains: [{ domain: 'example.com', dnsMode: 'external-dns' }], routes: [ { name: 'inbound-mail', match: { recipients: '*@example.com' }, - action: { type: 'process', process: { scan: true, dkim: true, queue: 'normal' } } - } - ] + action: { + type: 'forward', + forward: { host: 'mail-backend.example.com', port: 25 }, + }, + }, + ], }, - - // Authoritative DNS dnsNsDomains: ['ns1.example.com', 'ns2.example.com'], dnsScopes: ['example.com'], - publicIp: '203.0.113.1', - dnsRecords: [ - { name: 'example.com', type: 'A', value: '203.0.113.1' }, - { name: 'www.example.com', type: 'CNAME', value: 'example.com' } - ], - - // RADIUS authentication - radiusConfig: { - authPort: 1812, - acctPort: 1813, - clients: [ - { name: 'switch-1', ipRange: '192.168.1.0/24', secret: 'radius-secret', enabled: true } - ], - vlanAssignment: { - defaultVlan: 100, - allowUnknownMacs: true, - mappings: [ - { mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true }, - { mac: 'aa:bb:cc', vlan: 20, enabled: true } // OUI prefix - ] - }, - accounting: { enabled: true, retentionDays: 30 } - }, - - // Remote Ingress β€” edge nodes tunnel traffic to this hub - remoteIngressConfig: { - enabled: true, - tunnelPort: 8443, - hubDomain: 'hub.example.com', - }, - - // VPN β€” restrict sensitive routes to VPN clients + publicIp: '203.0.113.10', vpnConfig: { enabled: true, serverEndpoint: 'vpn.example.com', - clients: [ - { clientId: 'dev-laptop', serverDefinedClientTags: ['engineering'] }, - ], - }, - - // Unified database (embedded LocalSmartDb or external MongoDB) - dbConfig: { enabled: true }, - - // TLS & ACME - tls: { contactEmail: 'admin@example.com' }, - dnsChallenge: { cloudflareApiKey: process.env.CLOUDFLARE_API_KEY } -}); - -await router.start(); -// OpsServer dashboard available at http://localhost:3000 -``` - -## Architecture - -### System Overview - -```mermaid -graph TB - subgraph "External Traffic" - HTTP[HTTP/HTTPS Clients] - SMTP[SMTP Clients] - TCP[TCP Clients] - DNS[DNS Queries] - RAD[RADIUS Clients] - EDGE[Edge Nodes] - VPN[VPN Clients] - end - - subgraph "DcRouter Core" - DC[DcRouter Orchestrator] - SP[SmartProxy Engine
Rust-powered] - ES[smartmta Email Server
TypeScript + Rust] - DS[SmartDNS Server
Rust-powered] - RS[SmartRadius Server] - RI[RemoteIngress Hub
Rust data plane] - VS[SmartVPN Server
Rust data plane] - CM[Certificate Manager
smartacme v9] - OS[OpsServer Dashboard] - MM[Metrics Manager] - DB2[DcRouterDb
smartdata + smartdb] - end - - subgraph "Backend Services" - WEB[Web Services] - MAIL[Mail Servers] - DB[Databases] - API[Internal APIs] - end - - HTTP --> SP - TCP --> SP - SMTP --> ES - DNS --> DS - RAD --> RS - EDGE --> RI - VPN --> VS - - DC --> SP - DC --> ES - DC --> DS - DC --> RS - DC --> RI - DC --> VS - DC --> CM - DC --> OS - DC --> MM - DC --> DB2 - - SP --> WEB - SP --> API - ES --> MAIL - ES --> DB - RI --> SP - - CM -.-> SP - CM -.-> ES -``` - -### Core Components - -| Component | Package | Description | -|-----------|---------|-------------| -| **DcRouter** | `@serve.zone/dcrouter` | Central orchestrator β€” starts, stops, and coordinates all services | -| **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) | -| **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) | -| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) | -| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting | -| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting | -| **RemoteIngress** | `@serve.zone/remoteingress` | Distributed edge tunneling with Rust data plane and TS management | -| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management | -| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) | -| **DcRouterDb** | `@push.rocks/smartdata` + `@push.rocks/smartdb` | Unified database β€” embedded LocalSmartDb or external MongoDB for all persistence | - -### How It Works - -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 (default port 3000, configurable via `opsServerPort`), then spins up SmartProxy, smartmta, SmartDNS, SmartRadius, RemoteIngress, and SmartVPN based on which configs are provided. Services start in dependency order via `ServiceManager`. -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. SmartVPN runs a Rust data plane for WireGuard and custom transports. 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. - -### Rust-Powered Architecture - -DcRouter itself is a pure TypeScript orchestrator, but several of its core sub-components ship with **compiled Rust binaries** for performance-critical paths. At runtime each package detects the platform, unpacks the correct binary, and communicates with TypeScript over IPC/FFI β€” so you get the ergonomics of TypeScript with the throughput of native code. - -| Component | Rust Binary | What It Handles | -|-----------|-------------|-----------------| -| **SmartProxy** | `smartproxy-bin` | All TCP/TLS/HTTP proxy networking, NFTables integration, connection metrics | -| **smartmta** | `mailer-bin` | SMTP server + client, DKIM/SPF/DMARC, content scanning, IP reputation | -| **SmartDNS** | `smartdns-bin` | DNS server (UDP + DNS-over-HTTPS), DNSSEC, DNS client resolution | -| **RemoteIngress** | `remoteingress-bin` | Edge tunnel data plane, multiplexed streams, heartbeat management | -| **SmartVPN** | `smartvpn_daemon` | WireGuard (boringtun), Noise IK handshake, QUIC/WS transports, userspace NAT (smoltcp) | -| **SmartRadius** | β€” | Pure TypeScript (no Rust component) | - -## Configuration Reference - -### `IDcRouterOptions` - -```typescript -interface IDcRouterOptions { - // ── Base ─────────────────────────────────────────────────────── - /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ - baseDir?: string; - - // ── Traffic Routing ──────────────────────────────────────────── - /** SmartProxy config for HTTP/HTTPS and TCP/SNI routing */ - smartProxyConfig?: ISmartProxyOptions; - - // ── Email ────────────────────────────────────────────────────── - /** Unified email server configuration (smartmta) */ - emailConfig?: IUnifiedEmailServerOptions; - - /** Custom email port mapping overrides */ - emailPortConfig?: { - portMapping?: Record; - portSettings?: Record; - receivedEmailsPath?: string; - }; - - // ── DNS ──────────────────────────────────────────────────────── - /** Nameserver domains β€” get A records automatically */ - dnsNsDomains?: string[]; - /** Domains this server is authoritative for */ - dnsScopes?: string[]; - /** Public IP for NS A records */ - publicIp?: string; - /** Ingress proxy IPs (hides real server IP) */ - proxyIps?: string[]; - /** Custom DNS records */ - dnsRecords?: Array<{ - name: string; - type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA'; - value: string; - ttl?: number; - useIngressProxy?: boolean; - }>; - - // ── RADIUS ───────────────────────────────────────────────────── - /** RADIUS server for network authentication */ - radiusConfig?: { - authPort?: number; // default: 1812 - acctPort?: number; // default: 1813 - clients: IRadiusClient[]; - vlanAssignment?: IVlanManagerConfig; - accounting?: { enabled: boolean; retentionDays?: number }; - }; - - // ── Remote Ingress ───────────────────────────────────────────── - /** Remote Ingress hub for edge tunnel connections */ - remoteIngressConfig?: { - enabled?: boolean; // default: false - tunnelPort?: number; // default: 8443 - hubDomain?: string; // External hostname for connection tokens - tls?: { - certPath?: string; - keyPath?: string; - }; - }; - - // ── VPN ─────────────────────────────────────────────────────── - /** VPN server for route-level access control */ - vpnConfig?: { - enabled?: boolean; // default: false - subnet?: string; // default: '10.8.0.0/24' - wgListenPort?: number; // default: 51820 - dns?: string[]; // DNS servers pushed to VPN clients - serverEndpoint?: string; // Hostname in generated client configs - clients?: Array<{ // Pre-defined VPN clients - clientId: string; - serverDefinedClientTags?: string[]; - description?: string; - }>; - destinationPolicy?: { // Traffic routing policy - default: 'forceTarget' | 'block' | 'allow'; - target?: string; // IP for forceTarget (default: '127.0.0.1') - allowList?: string[]; // Pass through directly - blockList?: string[]; // Always block (overrides allowList) - }; - }; - - // ── 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; - domain?: string; - certPath?: string; - keyPath?: string; - }; - dnsChallenge?: { cloudflareApiKey?: string }; - - // ── Database ──────────────────────────────────────────────────── - /** Unified database for all persistence (routes, certs, VPN, RADIUS, etc.) */ - dbConfig?: { - enabled?: boolean; // default: true - mongoDbUrl?: string; // External MongoDB URL (omit for embedded LocalSmartDb) - storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb' - dbName?: string; // default: 'dcrouter' - cleanupIntervalHours?: number; // default: 1 - seedOnEmpty?: boolean; // Seed default profiles/targets if DB is empty - seedData?: object; // Custom seed data - }; -} -``` - -## HTTP/HTTPS & TCP/SNI Routing - -DcRouter uses [SmartProxy](https://code.foss.global/push.rocks/smartproxy) for all HTTP/HTTPS and TCP/SNI routing. Routes are pattern-matched by domain, port, or both. - -### HTTPS with Auto-TLS - -```typescript -{ - name: 'api-gateway', - match: { domains: ['api.example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.20', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' } - } -} -``` - -### TLS Passthrough (SNI Routing) - -```typescript -{ - name: 'secure-backend', - match: { domains: ['secure.example.com'], ports: [8443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.40', port: 8443 }], - tls: { mode: 'passthrough' } - } -} -``` - -### TCP Port Range Forwarding - -```typescript -{ - name: 'database-cluster', - match: { ports: [{ from: 5432, to: 5439 }] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.30', port: 'preserve' }], - security: { ipAllowList: ['192.168.1.0/24'] } - } -} -``` - -### HTTP Redirect - -```typescript -{ - name: 'http-to-https', - match: { ports: [80] }, - action: { type: 'redirect', redirect: { to: 'https://{domain}{path}' } } -} -``` - -## 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. - -### Email Domain Configuration - -Domains define _infrastructure_ β€” how DNS and DKIM are handled for each domain: - -#### Forward Mode -Simple forwarding without local DNS management: -```typescript -{ - domain: 'forwarded.com', - dnsMode: 'forward', - dns: { forward: { skipDnsValidation: true, targetDomain: 'mail.target.com' } } -} -``` - -#### Internal DNS Mode -Uses DcRouter's built-in DNS server (requires `dnsNsDomains` + `dnsScopes`): -```typescript -{ - domain: 'mail.example.com', - dnsMode: 'internal-dns', - dns: { internal: { mxPriority: 10, ttl: 3600 } }, - dkim: { selector: 'mail2024', keySize: 2048, rotateKeys: true, rotationInterval: 90 } -} -``` - -#### External DNS Mode -Uses existing DNS infrastructure with validation: -```typescript -{ - domain: 'mail.external.com', - dnsMode: 'external-dns', - dns: { external: { requiredRecords: ['MX', 'SPF', 'DKIM', 'DMARC'] } }, - rateLimits: { - inbound: { messagesPerMinute: 100, connectionsPerIp: 10 }, - outbound: { messagesPerMinute: 200 } - } -} -``` - -### Email Route Actions - -Routes define _behavior_ β€” what happens when an email matches: - -#### Forward πŸ“€ -Routes emails to an external SMTP server: -```typescript -{ - name: 'forward-to-internal', - match: { recipients: '*@company.com' }, - action: { - type: 'forward', - forward: { - host: 'internal-mail.company.com', - port: 25, - auth: { user: 'relay-user', pass: 'relay-pass' }, - addHeaders: { 'X-Forwarded-By': 'dcrouter' } - } - } -} -``` - -#### Process βš™οΈ -Full MTA processing with content scanning and delivery queues: -```typescript -{ - name: 'process-notifications', - match: { recipients: '*@notifications.company.com' }, - action: { - type: 'process', - process: { scan: true, dkim: true, queue: 'priority' } - } -} -``` - -#### Deliver πŸ“₯ -Local mailbox delivery: -```typescript -{ - name: 'deliver-local', - match: { recipients: '*@local.company.com' }, - action: { type: 'deliver' } -} -``` - -#### Reject 🚫 -Reject with custom SMTP response code: -```typescript -{ - name: 'reject-spam-domain', - match: { senders: '*@spam-domain.com', sizeRange: { min: 1000000 } }, - action: { - type: 'reject', - reject: { code: 550, message: 'Message rejected due to policy' } - } -} -``` - -### Route Matching - -Routes support powerful matching criteria: - -```typescript -// Recipient patterns -match: { recipients: '*@example.com' } // All addresses at domain -match: { recipients: 'admin@*' } // "admin" at any domain -match: { senders: ['*@trusted.com', '*@vip.com'] } // Multiple sender patterns - -// IP-based matching (CIDR) -match: { clientIp: '192.168.0.0/16' } -match: { clientIp: ['10.0.0.0/8', '172.16.0.0/12'] } - -// Authentication state -match: { authenticated: true } - -// Header matching -match: { headers: { 'X-Priority': 'high', 'Subject': /urgent|emergency/i } } - -// Size and content -match: { sizeRange: { min: 1000, max: 5000000 }, hasAttachments: true } -match: { subject: /invoice|receipt/i } -``` - -### Email Security Stack - -- **DKIM** β€” Automatic key generation, signing, and rotation for all domains -- **SPF** β€” Sender Policy Framework verification on inbound mail -- **DMARC** β€” Domain-based Message Authentication verification -- **IP Reputation** β€” Real-time IP reputation checking with caching -- **Content Scanning** β€” Spam, virus, and attachment scanning -- **Rate Limiting** β€” Hierarchical limits (global β†’ domain β†’ sender) -- **Bounce Management** β€” Automatic bounce detection and suppression lists - -### Email Deliverability - -- **IP Warmup Manager** β€” Multi-stage warmup schedules for new IPs -- **Sender Reputation Monitor** β€” Per-domain reputation tracking and scoring -- **Connection Pooling** β€” Pooled outbound SMTP connections per destination - -## DNS Server - -DcRouter includes an authoritative DNS server built on [smartdns](https://code.foss.global/push.rocks/smartdns). It handles standard UDP DNS on port 53 and DNS-over-HTTPS via SmartProxy socket handler. - -### Enabling DNS - -DNS is activated when both `dnsNsDomains` and `dnsScopes` are configured: - -```typescript -const router = new DcRouter({ - dnsNsDomains: ['ns1.example.com', 'ns2.example.com'], - dnsScopes: ['example.com'], - publicIp: '203.0.113.1', - dnsRecords: [ - { name: 'example.com', type: 'A', value: '203.0.113.1' }, - { name: 'www.example.com', type: 'CNAME', value: 'example.com' }, - { name: 'example.com', type: 'MX', value: '10:mail.example.com' }, - { name: 'example.com', type: 'TXT', value: 'v=spf1 a mx ~all' } - ] -}); -``` - -### Automatic DNS Records - -DcRouter auto-generates: -- **NS records** for all domains in `dnsScopes` -- **SOA records** for authoritative zones -- **A records** for nameserver domains (`dnsNsDomains`) -- **MX, SPF, DKIM, DMARC records** for email domains with `internal-dns` mode -- **ACME challenge records** for certificate provisioning - -### Ingress Proxy Support - -When `proxyIps` is configured, A records with `useIngressProxy: true` (default) will use the proxy IP instead of the real server IP β€” hiding your origin: - -```typescript -{ - proxyIps: ['198.51.100.1', '198.51.100.2'], - dnsRecords: [ - { name: 'example.com', type: 'A', value: '203.0.113.1' }, // Will resolve to 198.51.100.1 - { name: 'ns1.example.com', type: 'A', value: '203.0.113.1', useIngressProxy: false } // Stays real IP - ] -} -``` - -## RADIUS Server - -DcRouter includes a RADIUS server for network access control, built on [smartradius](https://code.foss.global/push.rocks/smartradius). - -### Configuration - -```typescript -const router = new DcRouter({ - radiusConfig: { - authPort: 1812, - acctPort: 1813, clients: [ { - name: 'core-switch', - ipRange: '192.168.1.0/24', - secret: 'shared-secret', - enabled: true - } + clientId: 'ops-laptop', + description: 'Operations laptop', + }, ], - vlanAssignment: { - defaultVlan: 100, - allowUnknownMacs: true, - mappings: [ - { mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true }, // Exact MAC - { mac: 'aa:bb:cc', vlan: 20, enabled: true }, // OUI prefix - ] - }, - accounting: { - enabled: true, - retentionDays: 30 - } - } -}); -``` - -### Components - -| Component | Purpose | -|-----------|---------| -| **RadiusServer** | Main RADIUS server handling auth + accounting requests | -| **VlanManager** | MAC-to-VLAN mapping with exact, OUI, and wildcard patterns | -| **AccountingManager** | Session tracking, traffic metering, start/stop/interim updates | - -### OpsServer API - -RADIUS is fully manageable at runtime via the OpsServer API: -- Client management (add/remove/list NAS devices) -- VLAN mapping CRUD operations -- Session monitoring and forced disconnects -- Accounting summaries and statistics - -## Remote Ingress - -DcRouter can act as a **hub** for distributed edge nodes using [`@serve.zone/remoteingress`](https://code.foss.global/serve.zone/remoteingress). Edge nodes accept incoming traffic at remote locations and tunnel it back to the hub over a single multiplexed connection. This is ideal for scenarios where you need to accept traffic at multiple geographic locations but process it centrally. - -### Enabling Remote Ingress - -```typescript -const router = new DcRouter({ - remoteIngressConfig: { + }, + dbConfig: { enabled: true, - tunnelPort: 8443, - hubDomain: 'hub.example.com', // Embedded in connection tokens }, - // Routes tagged with remoteIngress are auto-derived to edge listen ports - smartProxyConfig: { - routes: [ - { - name: 'web-via-edge', - match: { domains: ['app.example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.10', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' } - }, - remoteIngress: { enabled: true } // Edges will listen on port 443 - } - ] - } }); await router.start(); ``` -### Edge Registration +## Operations API and Dashboard -Edges are registered via the OpsServer API (or dashboard UI). Each edge gets a unique ID and secret: +With the database enabled, dcrouter exposes a management plane for: -```typescript -// Via TypedRequest API -const createReq = new TypedRequest( - 'https://hub:3000/typedrequest', 'createRemoteIngress' -); -const { edge } = await createReq.fire({ - identity, - name: 'edge-nyc-01', - autoDerivePorts: true, - tags: ['us-east'], -}); -// edge.secret is returned only on creation β€” save it! -``` +- routes and route toggles +- API tokens +- source profiles and network targets +- DNS providers, domains, and records +- ACME configuration and certificate lifecycle +- email domains and email operations +- VPN clients, remote ingress edges, and RADIUS data -### Connection Tokens πŸ”‘ +The browser dashboard is built from the `ts_web` package and is served by OpsServer. The same backend is accessible programmatically via TypedRequest or the dedicated API client package. -Instead of configuring edges with four separate values (hubHost, hubPort, edgeId, secret), DcRouter can generate a single **connection token** β€” an opaque base64url string that encodes everything: +## Programmatic API Client -```typescript -// Via TypedRequest API -const tokenReq = new TypedRequest( - 'https://hub:3000/typedrequest', 'getRemoteIngressConnectionToken' -); -const { token } = await tokenReq.fire({ identity, edgeId: 'edge-uuid' }); -// token = "eyJoIjoiaHViLmV4YW1wbGUuY29tIiwicCI6ODQ0MywiZSI6I..." - -// On the edge side, just pass the token: -const edge = new RemoteIngressEdge({ token }); -await edge.start(); -``` - -The token is generated using `remoteingress.encodeConnectionToken()` and contains `{ hubHost, hubPort, edgeId, secret }`. The `hubHost` comes from `remoteIngressConfig.hubDomain` (or can be overridden per-request). - -In the OpsServer dashboard, click **"Copy Token"** on any edge row to copy the connection token to your clipboard. - -### Auto-Derived Ports - -When routes have `remoteIngress: { enabled: true }`, edges with `autoDerivePorts: true` (default) automatically pick up those routes' ports. You can also use `edgeFilter` to restrict which edges get which ports: - -```typescript -{ - name: 'web-route', - match: { ports: [443] }, - action: { /* ... */ }, - remoteIngress: { - enabled: true, - edgeFilter: ['us-east', 'edge-uuid-123'] // Only edges with matching id or tags - } -} -``` - -### Dashboard Actions - -The OpsServer Remote Ingress view provides: - -| Action | Description | -|--------|-------------| -| **Create Edge Node** | Register a new edge with name, ports, tags | -| **Enable / Disable** | Toggle an edge on or off | -| **Edit** | Modify name, manual ports, auto-derive setting, tags | -| **Regenerate Secret** | Issue a new secret (invalidates the old one) | -| **Copy Token** | Generate and copy a base64url connection token to clipboard | -| **Delete** | Remove the edge registration | - -## VPN Access Control - -DcRouter integrates [`@push.rocks/smartvpn`](https://code.foss.global/push.rocks/smartvpn) to provide VPN-based route access control. VPN clients connect via standard WireGuard or native WebSocket/QUIC transports, receive an IP from a configurable subnet, and can then access routes that are restricted to VPN-only traffic. - -### How It Works - -1. **SmartVPN daemon** runs inside dcrouter with a Rust data plane (WireGuard via `boringtun`, custom protocol via Noise IK) -2. Clients connect and get assigned an IP from the VPN subnet (e.g. `10.8.0.0/24`) -3. **Smart split tunnel** β€” generated WireGuard configs auto-include the VPN subnet plus DNS-resolved IPs of VPN-gated domains. Domains from routes with `vpn.enabled` are resolved at config generation time, so clients route only the necessary traffic through the tunnel -4. Routes with `vpn: { enabled: true }` get `security.ipAllowList` dynamically injected (re-computed on every client change). With `mandatory: true` (default), the allowlist is replaced; with `mandatory: false`, VPN IPs are appended to existing rules -5. When `allowedServerDefinedClientTags` is set, only matching client IPs are injected (not the whole subnet) -6. SmartProxy enforces the allowlist β€” only authorized VPN clients can access protected routes -7. All VPN traffic is forced through SmartProxy via userspace NAT with PROXY protocol v2 β€” no root required - -### Destination Policy - -By default, VPN client traffic is redirected to localhost (SmartProxy) via `forceTarget`. You can customize this with a destination policy: - -```typescript -// Default: all traffic β†’ SmartProxy -destinationPolicy: { default: 'forceTarget', target: '127.0.0.1' } - -// Allow direct access to a backend subnet -destinationPolicy: { - default: 'forceTarget', - target: '127.0.0.1', - allowList: ['192.168.190.*'], // direct access to this subnet - blockList: ['192.168.190.1'], // except the gateway -} - -// Block everything except specific IPs -destinationPolicy: { - default: 'block', - allowList: ['10.0.0.*', '192.168.1.*'], -} -``` - -### Configuration - -```typescript -const router = new DcRouter({ - vpnConfig: { - enabled: true, - subnet: '10.8.0.0/24', // VPN client IP pool (default) - wgListenPort: 51820, // WireGuard UDP port (default) - serverEndpoint: 'vpn.example.com', // Hostname in generated client configs - dns: ['1.1.1.1', '8.8.8.8'], // DNS servers pushed to clients - - // Pre-define VPN clients with server-defined tags - clients: [ - { clientId: 'alice-laptop', serverDefinedClientTags: ['engineering'], description: 'Dev laptop' }, - { clientId: 'bob-phone', serverDefinedClientTags: ['engineering', 'mobile'] }, - { clientId: 'carol-desktop', serverDefinedClientTags: ['finance'] }, - ], - - // Optional: customize destination policy (default: forceTarget β†’ localhost) - // destinationPolicy: { default: 'forceTarget', target: '127.0.0.1', allowList: ['192.168.1.*'] }, - }, - smartProxyConfig: { - routes: [ - // πŸ” VPN-only: any VPN client can access - { - name: 'internal-app', - match: { domains: ['internal.example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.50', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' }, - }, - vpn: { enabled: true }, - }, - // πŸ” VPN + tag-restricted: only 'engineering' tagged clients - { - name: 'eng-dashboard', - match: { domains: ['eng.example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.51', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' }, - }, - vpn: { enabled: true, allowedServerDefinedClientTags: ['engineering'] }, - // β†’ alice + bob can access, carol cannot - }, - // 🌐 Public: no VPN - { - name: 'public-site', - match: { domains: ['example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.10', port: 80 }], - tls: { mode: 'terminate', certificate: 'auto' }, - }, - }, - ], - }, -}); -``` - -### Client Tags - -SmartVPN distinguishes between two types of client tags: - -| Tag Type | Set By | Purpose | -|----------|--------|---------| -| `serverDefinedClientTags` | Admin (via config or API) | **Trusted** β€” used for route access control | -| `clientDefinedClientTags` | Connecting client | **Informational** β€” displayed in dashboard, never used for security | - -Routes with `allowedServerDefinedClientTags` only permit VPN clients whose admin-assigned tags match. Clients cannot influence their own server-defined tags. - -### Client Management via OpsServer - -The OpsServer dashboard and API provide full VPN client lifecycle management: - -- **Create client** β€” generates WireGuard keypairs, assigns IP, returns a ready-to-use `.conf` file -- **QR code** β€” scan with the WireGuard mobile app (iOS/Android) for instant setup -- **Enable / Disable** β€” toggle client access without deleting -- **Rotate keys** β€” generate fresh keypairs (invalidates old ones) -- **Export config** β€” download in WireGuard (`.conf`), SmartVPN (`.json`), or scan as QR code -- **Telemetry** β€” per-client bytes sent/received, keepalives, rate limiting -- **Delete** β€” remove a client and revoke access - -Standard WireGuard clients on any platform (iOS, Android, macOS, Windows, Linux) can connect using the generated `.conf` file or by scanning the QR code β€” no custom VPN software needed. - -## Certificate Management - -DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions: - -### How It Works - -When a `dnsChallenge` is configured (e.g. with a Cloudflare API key), DcRouter creates a SmartAcme instance that handles DNS-01 challenges for automatic certificate provisioning. SmartProxy calls the `certProvisionFunction` whenever a route needs a TLS certificate, and SmartAcme takes care of the rest. - -```typescript -const router = new DcRouter({ - smartProxyConfig: { - routes: [ - { - name: 'secure-app', - match: { domains: ['app.example.com'], ports: [443] }, - action: { - type: 'forward', - targets: [{ host: '192.168.1.10', port: 8080 }], - tls: { mode: 'terminate', certificate: 'auto' } // ← triggers ACME provisioning - } - } - ], - acme: { email: 'admin@example.com', enabled: true, useProduction: true } - }, - tls: { contactEmail: 'admin@example.com' }, - dnsChallenge: { cloudflareApiKey: process.env.CLOUDFLARE_API_KEY } -}); -``` - -### smartacme v9 Features - -| Feature | Description | -|---------|-------------| -| **Per-domain deduplication** | Concurrent requests for the same domain share a single ACME operation | -| **Global concurrency cap** | Default 5 parallel ACME operations to prevent overload | -| **Account rate limiting** | Sliding window (250 orders / 3 hours) to stay within ACME provider limits | -| **Structured errors** | `AcmeError` with `isRetryable`, `isRateLimited`, `retryAfter` fields | -| **Clean shutdown** | `stop()` properly destroys HTTP agents and DNS clients | - -### Per-Domain Backoff - -DcRouter's `CertProvisionScheduler` adds **per-domain exponential backoff** on top of smartacme's built-in protections. If a DNS-01 challenge fails for a domain: - -1. The failure is recorded (persisted to storage) -2. The domain enters backoff: `min(failuresΒ² Γ— 1 hour, 24 hours)` -3. Subsequent requests for that domain are rejected until the backoff expires -4. On success, the backoff is cleared - -This prevents hammering ACME servers for domains with persistent issues (e.g. missing DNS delegation). - -### Fallback to HTTP-01 - -If DNS-01 fails, the `certProvisionFunction` returns `'http01'` to tell SmartProxy to fall back to HTTP-01 challenge validation. This provides a safety net for domains where DNS-01 isn't viable. - -### Certificate Storage - -Certificates are persisted via the `StorageBackedCertManager` which uses DcRouter's `StorageManager`. This means certs survive restarts and don't need to be re-provisioned unless they expire. - -### Dashboard - -The OpsServer includes a **Certificates** view showing: -- All domains with their certificate status (valid, expiring, expired, failed) -- Certificate source (ACME, provision function, static) -- Expiry dates and issuer information -- Backoff status for failed domains -- One-click reprovisioning per domain -- Certificate import and export - -## Storage & Database - -DcRouter uses a **unified database** (`DcRouterDb`) powered by [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) + [`@push.rocks/smartdb`](https://code.foss.global/push.rocks/smartdb) for all persistence. It supports two modes: - -### Embedded LocalSmartDb (Default) - -Zero-config, file-based MongoDB-compatible database β€” no external services needed: - -```typescript -dbConfig: { enabled: true } -// Data stored at ~/.serve.zone/dcrouter/tsmdb by default -``` - -### External MongoDB - -Connect to an existing MongoDB instance: - -```typescript -dbConfig: { - enabled: true, - mongoDbUrl: 'mongodb://localhost:27017', - dbName: 'dcrouter', -} -``` - -### Disabling the Database - -For static, constructor-only deployments where no runtime management is needed: - -```typescript -dbConfig: { enabled: false } -// Routes come exclusively from constructor config β€” no CRUD, no persistence -// OpsServer still runs but management features are disabled -``` - -### What's Stored - -DcRouterDb persists all runtime state across 15 document classes: - -| Category | Documents | Purpose | -|----------|-----------|---------| -| **Routes** | `StoredRouteDoc`, `RouteOverrideDoc` | Programmatic routes and hardcoded route overrides | -| **Certificates** | `ProxyCertDoc`, `AcmeCertDoc`, `CertBackoffDoc` | TLS certs, ACME state, per-domain backoff | -| **Auth** | `ApiTokenDoc` | API token storage | -| **Remote Ingress** | `RemoteIngressEdgeDoc` | Edge node registrations | -| **VPN** | `VpnServerKeysDoc`, `VpnClientDoc` | Server keys and client registrations | -| **RADIUS** | `VlanMappingsDoc`, `AccountingSessionDoc` | VLAN mappings and accounting sessions | -| **References** | `SecurityProfileDoc`, `NetworkTargetDoc` | Reusable security profiles and network targets | -| **Cache** | `CachedEmailDoc`, `CachedIpReputationDoc` | TTL-based caches with automatic cleanup | - -## Security Features - -### IP Reputation Checking - -Automatic IP reputation checks on inbound connections with configurable caching: - -```typescript -// IP reputation is checked automatically for inbound SMTP connections. -// Results are cached according to cacheConfig.ttlConfig.ipReputation. -``` - -### Rate Limiting - -Hierarchical rate limits with three levels of specificity: - -```typescript -// Global defaults (via emailConfig.defaults.rateLimits) -defaults: { - rateLimits: { - inbound: { messagesPerMinute: 50, connectionsPerIp: 5, recipientsPerMessage: 50 }, - outbound: { messagesPerMinute: 100 } - } -} - -// Per-domain overrides (in domain config) -{ - domain: 'high-volume.com', - rateLimits: { - outbound: { messagesPerMinute: 500 } // Override for this domain - } -} -``` - -**Precedence**: Domain-specific > Pattern-specific > Global - -### Content Scanning - -```typescript -action: { - type: 'process', - options: { - contentScanning: true, - scanners: [ - { type: 'spam', threshold: 5.0, action: 'tag' }, - { type: 'virus', action: 'reject' }, - { type: 'attachment', blockedExtensions: ['.exe', '.bat', '.scr'], action: 'reject' } - ] - } -} -``` - -## OpsServer Dashboard - -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 | Description | -|------|-------------| -| πŸ“Š **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput | -| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics | -| πŸ“§ **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents | -| πŸ›£οΈ **Routes** | Merged route list (hardcoded + programmatic), create/edit/toggle/override routes | -| πŸ”‘ **API Tokens** | Token management with scopes, create/revoke/roll/toggle | -| πŸ” **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export | -| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable | -| πŸ” **VPN** | VPN client management, server status, create/toggle/export/rotate/delete clients | -| πŸ›‘οΈ **Security Profiles** | Reusable security configurations (IP allow/block lists, rate limits) | -| 🎯 **Network Targets** | Reusable host:port destinations for route references | -| πŸ“‘ **RADIUS** | NAS client management, VLAN mappings, session monitoring, accounting | -| πŸ“œ **Logs** | Real-time log viewer with level filtering and search | -| βš™οΈ **Configuration** | Read-only view of current system configuration | -| πŸ›‘οΈ **Security** | IP reputation, rate limit status, blocked connections | - -### API Endpoints - -All management is done via TypedRequest over HTTP POST to `/typedrequest`: - -```typescript -// Authentication -'adminLoginWithUsernameAndPassword' // Login with credentials β†’ returns JWT identity -'verifyIdentity' // Verify JWT token validity -'adminLogout' // End admin session - -// Statistics & Health -'getServerStatistics' // Uptime, CPU, memory, connections -'getHealthStatus' // System health check -'getCombinedMetrics' // All metrics in one call - -// Email Operations -'getAllEmails' // List all emails (queued/sent/failed) -'getEmailDetail' // Full detail for a specific email -'resendEmail' // Re-queue a failed email - -// Certificates -'getCertificateOverview' // Domain-centric certificate status -'reprovisionCertificate' // Reprovision by route name (legacy) -'reprovisionCertificateDomain' // Reprovision by domain (preferred) -'importCertificate' // Import a certificate -'exportCertificate' // Export a certificate -'deleteCertificate' // Delete a certificate - -// Remote Ingress -'getRemoteIngresses' // List all edge registrations -'createRemoteIngress' // Register a new edge -'updateRemoteIngress' // Update edge settings -'deleteRemoteIngress' // Remove an edge -'regenerateRemoteIngressSecret' // Issue a new secret -'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 -'getRecentLogs' // Retrieve system logs with filtering -'getLogStream' // Stream live logs - -// VPN -'getVpnClients' // List all registered VPN clients -'getVpnStatus' // VPN server status (running, subnet, port, keys) -'createVpnClient' // Create client β†’ returns WireGuard config (shown once) -'deleteVpnClient' // Remove a VPN client -'enableVpnClient' // Enable a disabled client -'disableVpnClient' // Disable a client -'rotateVpnClientKey' // Generate new keys (invalidates old ones) -'exportVpnClientConfig' // Export WireGuard (.conf) or SmartVPN (.json) config -'getVpnClientTelemetry' // Per-client bytes sent/received, keepalives - -// RADIUS -'getRadiusSessions' // Active RADIUS sessions -'getRadiusClients' // List NAS clients -'getRadiusStatistics' // RADIUS stats -'setRadiusClient' // Add/update NAS client -'removeRadiusClient' // Remove NAS client -'getVlanMappings' // List VLAN mappings -'setVlanMapping' // Add/update VLAN mapping -'removeVlanMapping' // Remove VLAN mapping -'testVlanAssignment' // Test what VLAN a MAC gets - -// Security Profiles -'getSecurityProfiles' // List all security profiles -'getSecurityProfile' // Get a single profile by ID -'createSecurityProfile' // Create a reusable security profile -'updateSecurityProfile' // Update a profile (propagates to referencing routes) -'deleteSecurityProfile' // Delete a profile (with optional force) -'getSecurityProfileUsage' // Get routes referencing a profile - -// Network Targets -'getNetworkTargets' // List all network targets -'getNetworkTarget' // Get a single target by ID -'createNetworkTarget' // Create a reusable host:port target -'updateNetworkTarget' // Update a target (propagates to referencing routes) -'deleteNetworkTarget' // Delete a target (with optional force) -'getNetworkTargetUsage' // Get routes referencing a target -``` - -## 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: +Use the API client when you want automation or integration code instead of clicking through the dashboard. ```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' }); +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); +const systemRoutes = routes.filter((route) => route.origin !== 'api'); -// Builder pattern for creation -const newRoute = await client.routes.build() +if (systemRoutes[0]) { + await systemRoutes[0].toggle(false); +} + +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' }) + .setAction({ type: 'forward', targets: [{ host: '127.0.0.1', port: 8081 }] }) .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 +See `./ts_apiclient/readme.md` for the dedicated API-client package docs. -| 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()` | +## Published Modules -See the [full API client documentation](./ts_apiclient/readme.md) for detailed usage of every manager, builder, and resource class. +This repository publishes multiple modules from the same codebase. -## API Reference +| Module | Purpose | Docs | +| --- | --- | --- | +| `@serve.zone/dcrouter` | Main orchestrator and server package | `./readme.md` | +| `@serve.zone/dcrouter-interfaces` | Shared TypedRequest request and data interfaces | `./ts_interfaces/readme.md` | +| `@serve.zone/dcrouter-migrations` | Startup migration runner for dcrouter data | `./ts_migrations/readme.md` | +| `@serve.zone/dcrouter-web` | Web dashboard entry and UI components | `./ts_web/readme.md` | +| `@serve.zone/dcrouter-apiclient` | Typed OO API client | `./ts_apiclient/readme.md` | -### DcRouter Class - -```typescript -import { DcRouter } from '@serve.zone/dcrouter'; - -const router = new DcRouter(options: IDcRouterOptions); -``` - -#### Methods - -| Method | Description | -|--------|-------------| -| `start(): Promise` | Start all configured services | -| `stop(): Promise` | Gracefully stop all services | -| `updateSmartProxyConfig(config): Promise` | Hot-update SmartProxy routes | -| `updateEmailConfig(config): Promise` | Hot-update email configuration | -| `updateEmailRoutes(routes): Promise` | Update email routing rules at runtime | -| `updateRadiusConfig(config): Promise` | Hot-update RADIUS configuration | -| `getStats(): any` | Get real-time statistics from all services | - -#### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `options` | `IDcRouterOptions` | Current configuration | -| `smartProxy` | `SmartProxy` | SmartProxy instance | -| `smartAcme` | `SmartAcme` | SmartAcme v9 certificate manager instance | -| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) | -| `dnsServer` | `DnsServer` | DNS server instance | -| `radiusServer` | `RadiusServer` | RADIUS server instance | -| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager | -| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager | -| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager | -| `opsServer` | `OpsServer` | OpsServer/dashboard instance | -| `metricsManager` | `MetricsManager` | Metrics collector | -| `dcRouterDb` | `DcRouterDb` | Unified database instance (smartdata + smartdb) | -| `routeConfigManager` | `RouteConfigManager` | Programmatic route CRUD manager | -| `apiTokenManager` | `ApiTokenManager` | API token management | -| `referenceResolver` | `ReferenceResolver` | Security profile and network target resolver | - -### Re-exported Types - -DcRouter re-exports key types for convenience: - -```typescript -import { - DcRouter, - IDcRouterOptions, - UnifiedEmailServer, - type IUnifiedEmailServerOptions, - type IEmailRoute, - type IEmailDomainConfig, - type IHttp3Config, -} from '@serve.zone/dcrouter'; -``` - -## Sub-Modules - -DcRouter is published as a monorepo with separately-installable interface and web packages: - -| Package | Description | Install | -|---------|-------------|---------| -| [`@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 directly from the main package: - -```typescript -import { data, requests } from '@serve.zone/dcrouter/interfaces'; -import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient'; -``` - -## Testing - -DcRouter includes a comprehensive test suite covering all system components: +## Development and Testing ```bash -# Run all tests +pnpm run build pnpm test - -# Run a specific test file -tstest test/test.jwt-auth.ts --verbose - -# Run with extended timeout -tstest test/test.opsserver-api.ts --verbose --timeout 60 ``` -### Test Coverage - -| 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 | 8 | -| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 | -| `test.reference-resolver.ts` | Security profiles, network targets, route resolution | 20 | -| `test.security-profiles-api.ts` | Profile/target API endpoints, auth enforcement | 13 | - -## Docker / OCI Container Deployment - -DcRouter ships with a production-ready `Dockerfile` and supports environment-variable-driven configuration for OCI container deployments. The container image includes tini as PID 1 (via the base image), proper health checks, and configurable resource limits. When `DCROUTER_MODE=OCI_CONTAINER` is set, DcRouter automatically reads configuration from environment variables (and optionally from a JSON config file). - -### Running with Docker +Target a single test file while working on one area: ```bash -docker run -d \ - --ulimit nofile=65536:65536 \ - -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 587:587 -p 465:465 \ - -p 53:53/udp -p 3000:3000 -p 8443:8443 \ - code.foss.global/serve.zone/dcrouter:latest +tstest test/test.dns-runtime-routes.node.ts --verbose ``` -> ⚑ **Production tip:** Always set `--ulimit nofile=65536:65536` for production deployments. DcRouter will log a warning at startup if the file descriptor limit is below 65536. +## Notes for Operators -### Environment Variables - -| Variable | Description | Default | Example | -|----------|-------------|---------|---------| -| `DCROUTER_MODE` | Container mode (set automatically in image) | `OCI_CONTAINER` | β€” | -| `DCROUTER_CONFIG_PATH` | Path to JSON config file (env vars override) | β€” | `/config/dcrouter.json` | -| `DCROUTER_BASE_DIR` | Base data directory | `~/.serve.zone/dcrouter` | `/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` | `false` | -| `DCROUTER_HEAP_SIZE` | Node.js V8 heap size in MB | `512` | `1024` | -| `DCROUTER_MAX_CONNECTIONS` | Global max concurrent connections | `50000` | `100000` | -| `DCROUTER_MAX_CONNECTIONS_PER_IP` | Max connections per source IP | `100` | `200` | -| `DCROUTER_CONNECTION_RATE_LIMIT` | Max new connections/min per IP | `600` | `1200` | - -### Exposed Ports - -The container exposes all service ports: - -| Port(s) | Protocol | Service | -|---------|----------|---------| -| 80, 443 | TCP | HTTP/HTTPS (SmartProxy) | -| 25, 587, 465 | TCP | SMTP, Submission, SMTPS | -| 53 | TCP/UDP | DNS | -| 1812, 1813 | UDP | RADIUS auth/acct | -| 3000 | TCP | OpsServer dashboard | -| 8443 | TCP | Remote ingress tunnels | -| 51820 | UDP | WireGuard VPN | -| 29000–30000 | TCP | Dynamic port range | - -### 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). +- Database-backed management features depend on `dbConfig.enabled !== false`. +- If you disable the DB, constructor-configured services still run, but persistent management features are limited. +- Nameserver domains are still required for DNS bootstrap and DoH route generation. +- HTTP/3 is enabled by default for qualifying HTTPS routes unless disabled globally or per route. ## License and Legal Information diff --git a/test/test.dns-runtime-routes.node.ts b/test/test.dns-runtime-routes.node.ts index d91c21e..87d99de 100644 --- a/test/test.dns-runtime-routes.node.ts +++ b/test/test.dns-runtime-routes.node.ts @@ -40,7 +40,7 @@ const clearTestState = async () => { } }; -tap.test('RouteConfigManager applies runtime DoH routes without persisting them', async () => { +tap.test('RouteConfigManager persists DoH system routes and hydrates runtime socket handlers', async () => { await testDbPromise; await clearTestState(); @@ -64,15 +64,24 @@ tap.test('RouteConfigManager applies runtime DoH routes without persisting them' undefined, undefined, undefined, - () => (dcRouter as any).generateDnsRoutes(), + undefined, + (storedRoute: any) => (dcRouter as any).hydrateStoredRouteForRuntime(storedRoute), ); - await routeManager.initialize([], [], []); - await routeManager.applyRoutes(); + await routeManager.initialize([], [], (dcRouter as any).generateDnsRoutes({ includeSocketHandler: false })); const persistedRoutes = await RouteDoc.findAll(); - expect(persistedRoutes.length).toEqual(0); - expect(appliedRoutes.length).toEqual(2); + expect(persistedRoutes.length).toEqual(2); + expect(persistedRoutes.every((route) => route.origin === 'dns')).toEqual(true); + expect((await RouteDoc.findByName('dns-over-https-dns-query'))?.systemKey).toEqual('dns:dns-over-https-dns-query'); + expect((await RouteDoc.findByName('dns-over-https-resolve'))?.systemKey).toEqual('dns:dns-over-https-resolve'); + + const mergedRoutes = routeManager.getMergedRoutes().routes; + expect(mergedRoutes.length).toEqual(2); + expect(mergedRoutes.every((route) => route.origin === 'dns')).toEqual(true); + expect(mergedRoutes.every((route) => route.systemKey?.startsWith('dns:'))).toEqual(true); + + expect(appliedRoutes.length).toEqual(1); for (const routeSet of appliedRoutes) { const dnsQueryRoute = routeSet.find((route) => route.name === 'dns-over-https-dns-query'); @@ -85,10 +94,17 @@ tap.test('RouteConfigManager applies runtime DoH routes without persisting them' } }); -tap.test('RouteConfigManager removes stale persisted DoH socket-handler routes on startup', async () => { +tap.test('RouteConfigManager backfills existing DoH system routes by name without duplicating them', async () => { await testDbPromise; await clearTestState(); + const dcRouter = new DcRouter({ + dnsNsDomains: ['ns1.example.com', 'ns2.example.com'], + dnsScopes: ['example.com'], + smartProxyConfig: { routes: [] }, + dbConfig: { enabled: false }, + }); + const staleDnsQueryRoute = new RouteDoc(); staleDnsQueryRoute.id = 'stale-doh-query'; staleDnsQueryRoute.route = { @@ -109,47 +125,6 @@ tap.test('RouteConfigManager removes stale persisted DoH socket-handler routes o staleDnsQueryRoute.origin = 'dns'; await staleDnsQueryRoute.save(); - const staleResolveRoute = new RouteDoc(); - staleResolveRoute.id = 'stale-doh-resolve'; - staleResolveRoute.route = { - name: 'dns-over-https-resolve', - match: { - ports: [443], - domains: ['ns1.example.com'], - path: '/resolve', - }, - action: { - type: 'socket-handler' as any, - } as any, - }; - staleResolveRoute.enabled = true; - staleResolveRoute.createdAt = Date.now(); - staleResolveRoute.updatedAt = Date.now(); - staleResolveRoute.createdBy = 'test'; - staleResolveRoute.origin = 'dns'; - await staleResolveRoute.save(); - - const validRoute = new RouteDoc(); - validRoute.id = 'valid-forward-route'; - validRoute.route = { - name: 'valid-forward-route', - match: { - ports: [443], - domains: ['app.example.com'], - }, - action: { - type: 'forward', - targets: [{ host: '127.0.0.1', port: 8443 }], - tls: { mode: 'terminate' as const }, - }, - } as any; - validRoute.enabled = true; - validRoute.createdAt = Date.now(); - validRoute.updatedAt = Date.now(); - validRoute.createdBy = 'test'; - validRoute.origin = 'api'; - await validRoute.save(); - const appliedRoutes: any[][] = []; const smartProxy = { updateRoutes: async (routes: any[]) => { @@ -157,19 +132,76 @@ tap.test('RouteConfigManager removes stale persisted DoH socket-handler routes o }, }; - const routeManager = new RouteConfigManager(() => smartProxy as any); - await routeManager.initialize([], [], []); - - expect((await RouteDoc.findByName('dns-over-https-dns-query'))).toEqual(null); - expect((await RouteDoc.findByName('dns-over-https-resolve'))).toEqual(null); + const routeManager = new RouteConfigManager( + () => smartProxy as any, + undefined, + undefined, + undefined, + undefined, + undefined, + (storedRoute: any) => (dcRouter as any).hydrateStoredRouteForRuntime(storedRoute), + ); + await routeManager.initialize([], [], (dcRouter as any).generateDnsRoutes({ includeSocketHandler: false })); const remainingRoutes = await RouteDoc.findAll(); - expect(remainingRoutes.length).toEqual(1); - expect(remainingRoutes[0].route.name).toEqual('valid-forward-route'); + expect(remainingRoutes.length).toEqual(2); + expect(remainingRoutes.filter((route) => route.route.name === 'dns-over-https-dns-query').length).toEqual(1); + expect(remainingRoutes.filter((route) => route.route.name === 'dns-over-https-resolve').length).toEqual(1); + + const queryRoute = await RouteDoc.findByName('dns-over-https-dns-query'); + expect(queryRoute?.id).toEqual('stale-doh-query'); + expect(queryRoute?.systemKey).toEqual('dns:dns-over-https-dns-query'); + + const resolveRoute = await RouteDoc.findByName('dns-over-https-resolve'); + expect(resolveRoute?.systemKey).toEqual('dns:dns-over-https-resolve'); expect(appliedRoutes.length).toEqual(1); - expect(appliedRoutes[0].length).toEqual(1); - expect(appliedRoutes[0][0].name).toEqual('valid-forward-route'); + expect(appliedRoutes[0].length).toEqual(2); + expect(appliedRoutes[0].every((route) => typeof route.action.socketHandler === 'function')).toEqual(true); +}); + +tap.test('RouteConfigManager only allows toggling system routes', async () => { + await testDbPromise; + await clearTestState(); + + const smartProxy = { + updateRoutes: async (_routes: any[]) => { + return; + }, + }; + + const routeManager = new RouteConfigManager(() => smartProxy as any); + await routeManager.initialize([ + { + name: 'system-config-route', + match: { + ports: [443], + domains: ['app.example.com'], + }, + action: { + type: 'forward', + targets: [{ host: '127.0.0.1', port: 8443 }], + tls: { mode: 'terminate' as const }, + }, + } as any, + ], [], []); + + const systemRoute = routeManager.getMergedRoutes().routes.find((route) => route.route.name === 'system-config-route'); + expect(systemRoute).toBeDefined(); + + const updateResult = await routeManager.updateRoute(systemRoute!.id, { + route: { name: 'renamed-system-route' } as any, + }); + expect(updateResult.success).toEqual(false); + expect(updateResult.message).toEqual('System routes are managed by the system and can only be toggled'); + + const deleteResult = await routeManager.deleteRoute(systemRoute!.id); + expect(deleteResult.success).toEqual(false); + expect(deleteResult.message).toEqual('System routes are managed by the system and cannot be deleted'); + + const toggleResult = await routeManager.toggleRoute(systemRoute!.id, false); + expect(toggleResult.success).toEqual(true); + expect((await RouteDoc.findById(systemRoute!.id))?.enabled).toEqual(false); }); tap.test('DnsManager warning keeps dnsNsDomains in scope', async () => { diff --git a/test/test.email-dns-records.node.ts b/test/test.email-dns-records.node.ts new file mode 100644 index 0000000..8944a28 --- /dev/null +++ b/test/test.email-dns-records.node.ts @@ -0,0 +1,65 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { buildEmailDnsRecords } from '../ts/email/index.js'; + +tap.test('buildEmailDnsRecords uses the configured mail hostname for MX and includes DKIM when provided', async () => { + const records = buildEmailDnsRecords({ + domain: 'example.com', + hostname: 'mail.example.com', + selector: 'selector1', + dkimValue: 'v=DKIM1; h=sha256; k=rsa; p=abc123', + statuses: { + mx: 'valid', + spf: 'missing', + dkim: 'valid', + dmarc: 'unchecked', + }, + }); + + expect(records).toEqual([ + { + type: 'MX', + name: 'example.com', + value: '10 mail.example.com', + status: 'valid', + }, + { + type: 'TXT', + name: 'example.com', + value: 'v=spf1 a mx ~all', + status: 'missing', + }, + { + type: 'TXT', + name: 'selector1._domainkey.example.com', + value: 'v=DKIM1; h=sha256; k=rsa; p=abc123', + status: 'valid', + }, + { + type: 'TXT', + name: '_dmarc.example.com', + value: 'v=DMARC1; p=none; rua=mailto:dmarc@example.com', + status: 'unchecked', + }, + ]); +}); + +tap.test('buildEmailDnsRecords omits DKIM when no value is provided', async () => { + const records = buildEmailDnsRecords({ + domain: 'example.net', + hostname: 'smtp.example.net', + mxPriority: 20, + }); + + expect(records.map((record) => record.name)).toEqual([ + 'example.net', + 'example.net', + '_dmarc.example.net', + ]); + expect(records[0].value).toEqual('20 smtp.example.net'); +}); + +tap.test('cleanup', async () => { + await tap.stopForcefully(); +}); + +export default tap.start(); diff --git a/test/test.email-ops-api.ts b/test/test.email-ops-api.ts new file mode 100644 index 0000000..d73ffb2 --- /dev/null +++ b/test/test.email-ops-api.ts @@ -0,0 +1,167 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { TypedRequest } from '@api.global/typedrequest'; +import { DcRouter } from '../ts/index.js'; +import * as interfaces from '../ts_interfaces/index.js'; + +const TEST_PORT = 3201; +const BASE_URL = `http://localhost:${TEST_PORT}/typedrequest`; + +let testDcRouter: DcRouter; +let adminIdentity: interfaces.data.IIdentity; +let removedQueueItemId: string | undefined; +let lastEnqueueArgs: any[] | undefined; + +const queueItems = [ + { + id: 'failed-email-1', + status: 'failed', + attempts: 3, + nextAttempt: new Date('2026-04-14T10:00:00.000Z'), + lastError: '550 mailbox unavailable', + processingMode: 'mta', + route: undefined, + createdAt: new Date('2026-04-14T09:00:00.000Z'), + processingResult: { + from: 'sender@example.com', + to: ['recipient@example.net'], + cc: ['copy@example.net'], + subject: 'Older message', + text: 'hello', + headers: { 'x-test': '1' }, + getMessageId: () => 'message-older', + getAttachmentsSize: () => 64, + }, + }, + { + id: 'delivered-email-1', + status: 'delivered', + attempts: 1, + processingMode: 'mta', + route: undefined, + createdAt: new Date('2026-04-14T11:00:00.000Z'), + processingResult: { + email: { + from: 'fresh@example.com', + to: ['new@example.net'], + cc: [], + subject: 'Newest message', + }, + html: '

newest

', + text: 'newest', + headers: { 'x-fresh': 'true' }, + getMessageId: () => 'message-newer', + getAttachmentsSize: () => 0, + }, + }, +]; + +tap.test('should start DCRouter with OpsServer for email API tests', async () => { + testDcRouter = new DcRouter({ + opsServerPort: TEST_PORT, + dbConfig: { enabled: false }, + }); + + await testDcRouter.start(); + testDcRouter.emailServer = { + getQueueItems: () => [...queueItems], + getQueueItem: (id: string) => queueItems.find((item) => item.id === id), + getQueueStats: () => ({ + queueSize: 2, + status: { + pending: 0, + processing: 1, + failed: 1, + deferred: 1, + delivered: 1, + }, + }), + deliveryQueue: { + enqueue: async (...args: any[]) => { + lastEnqueueArgs = args; + return 'resent-queue-id'; + }, + removeItem: async (id: string) => { + removedQueueItemId = id; + return true; + }, + }, + } as any; + + expect(testDcRouter.opsServer).toBeInstanceOf(Object); +}); + +tap.test('should login as admin for email API tests', async () => { + const loginRequest = new TypedRequest( + BASE_URL, + 'adminLoginWithUsernameAndPassword', + ); + + const response = await loginRequest.fire({ + username: 'admin', + password: 'admin', + }); + + adminIdentity = response.identity; + expect(adminIdentity.jwt).toBeTruthy(); +}); + +tap.test('should return queued emails through the email ops API', async () => { + const request = new TypedRequest(BASE_URL, 'getAllEmails'); + const response = await request.fire({ + identity: adminIdentity, + }); + + expect(response.emails.map((email) => email.id)).toEqual(['delivered-email-1', 'failed-email-1']); + expect(response.emails[0].status).toEqual('delivered'); + expect(response.emails[1].status).toEqual('bounced'); +}); + +tap.test('should return email detail through the email ops API', async () => { + const request = new TypedRequest(BASE_URL, 'getEmailDetail'); + const response = await request.fire({ + identity: adminIdentity, + emailId: 'failed-email-1', + }); + + expect(response.email?.toList).toEqual(['recipient@example.net']); + expect(response.email?.cc).toEqual(['copy@example.net']); + expect(response.email?.rejectionReason).toEqual('550 mailbox unavailable'); + expect(response.email?.headers).toEqual({ 'x-test': '1' }); +}); + +tap.test('should expose queue status through the stats API', async () => { + const request = new TypedRequest(BASE_URL, 'getQueueStatus'); + const response = await request.fire({ + identity: adminIdentity, + }); + + expect(response.queues.length).toEqual(1); + expect(response.queues[0].size).toEqual(0); + expect(response.queues[0].processing).toEqual(1); + expect(response.queues[0].failed).toEqual(1); + expect(response.queues[0].retrying).toEqual(1); + expect(response.totalItems).toEqual(3); +}); + +tap.test('should resend failed email through the admin email ops API', async () => { + const request = new TypedRequest(BASE_URL, 'resendEmail'); + const response = await request.fire({ + identity: adminIdentity, + emailId: 'failed-email-1', + }); + + expect(response.success).toEqual(true); + expect(response.newQueueId).toEqual('resent-queue-id'); + expect(removedQueueItemId).toEqual('failed-email-1'); + expect(lastEnqueueArgs?.[0]).toEqual(queueItems[0].processingResult); +}); + +tap.test('should stop DCRouter after email API tests', async () => { + await testDcRouter.stop(); +}); + +tap.test('cleanup', async () => { + await tap.stopForcefully(); +}); + +export default tap.start(); diff --git a/test/test.email-ops-handlers.node.ts b/test/test.email-ops-handlers.node.ts new file mode 100644 index 0000000..774cf87 --- /dev/null +++ b/test/test.email-ops-handlers.node.ts @@ -0,0 +1,107 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { EmailOpsHandler } from '../ts/opsserver/handlers/email-ops.handler.js'; +import { StatsHandler } from '../ts/opsserver/handlers/stats.handler.js'; + +const createRouterStub = () => ({ + addTypedHandler: (_handler: unknown) => {}, +}); + +const queueItems = [ + { + id: 'older-failed', + status: 'failed', + attempts: 3, + nextAttempt: new Date('2026-04-14T10:00:00.000Z'), + lastError: '550 mailbox unavailable', + createdAt: new Date('2026-04-14T09:00:00.000Z'), + processingResult: { + from: 'sender@example.com', + to: ['recipient@example.net'], + cc: ['copy@example.net'], + subject: 'Older message', + text: 'hello', + headers: { 'x-test': '1' }, + getMessageId: () => 'message-older', + getAttachmentsSize: () => 64, + }, + }, + { + id: 'newer-delivered', + status: 'delivered', + attempts: 1, + createdAt: new Date('2026-04-14T11:00:00.000Z'), + processingResult: { + email: { + from: 'fresh@example.com', + to: ['new@example.net'], + cc: [], + subject: 'Newest message', + }, + html: '

newest

', + text: 'newest', + headers: { 'x-fresh': 'true' }, + getMessageId: () => 'message-newer', + getAttachmentsSize: () => 0, + }, + }, +]; + +tap.test('EmailOpsHandler maps queue items using public email server APIs', async () => { + const opsHandler = new EmailOpsHandler({ + viewRouter: createRouterStub(), + adminRouter: createRouterStub(), + dcRouterRef: { + emailServer: { + getQueueItems: () => queueItems, + getQueueItem: (id: string) => queueItems.find((item) => item.id === id), + }, + }, + } as any); + + const emails = (opsHandler as any).getAllQueueEmails(); + expect(emails.map((email: any) => email.id)).toEqual(['newer-delivered', 'older-failed']); + expect(emails[0].status).toEqual('delivered'); + expect(emails[1].status).toEqual('bounced'); + expect(emails[0].messageId).toEqual('message-newer'); + + const detail = (opsHandler as any).getEmailDetail('older-failed'); + expect(detail?.toList).toEqual(['recipient@example.net']); + expect(detail?.cc).toEqual(['copy@example.net']); + expect(detail?.rejectionReason).toEqual('550 mailbox unavailable'); + expect(detail?.headers).toEqual({ 'x-test': '1' }); +}); + +tap.test('StatsHandler reports queue status using public email server APIs', async () => { + const statsHandler = new StatsHandler({ + viewRouter: createRouterStub(), + dcRouterRef: { + emailServer: { + getQueueStats: () => ({ + queueSize: 2, + status: { + pending: 0, + processing: 1, + failed: 1, + deferred: 1, + delivered: 1, + }, + }), + getQueueItems: () => queueItems, + }, + }, + } as any); + + const queueStatus = await (statsHandler as any).getQueueStatus(); + expect(queueStatus.pending).toEqual(0); + expect(queueStatus.active).toEqual(1); + expect(queueStatus.failed).toEqual(1); + expect(queueStatus.retrying).toEqual(1); + expect(queueStatus.items.map((item: any) => item.id)).toEqual(['newer-delivered', 'older-failed']); + expect(queueStatus.items[1].nextRetry).toEqual(new Date('2026-04-14T10:00:00.000Z').getTime()); +}); + +tap.test('cleanup', async () => { + await tap.stopForcefully(); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index f9f1930..74c6bb3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.18.0', + version: '13.19.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index cb43738..14466f3 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -30,7 +30,8 @@ import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/ import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js'; import { DnsManager } from './dns/manager.dns.js'; import { AcmeConfigManager } from './acme/manager.acme-config.js'; -import { EmailDomainManager, SmartMtaStorageManager } from './email/index.js'; +import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js'; +import type { IRoute } from '../ts_interfaces/data/route-management.js'; export interface IDcRouterOptions { /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */ @@ -314,7 +315,8 @@ export class DcRouter { // Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = []; private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = []; - // Runtime-only DoH routes. These carry live socket handlers and must never be persisted. + private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = []; + // Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes. private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = []; // Environment access @@ -588,13 +590,15 @@ export class DcRouter { this.tunnelManager.syncAllowedEdges(); } }, - () => this.runtimeDnsRoutes, + undefined, + (storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute), ); this.apiTokenManager = new ApiTokenManager(); await this.apiTokenManager.initialize(); await this.routeConfigManager.initialize( this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[], this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[], + this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[], ); await this.targetProfileManager.normalizeAllRouteRefs(); @@ -912,10 +916,12 @@ export class DcRouter { logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) }); } + this.seedDnsRoutes = []; this.runtimeDnsRoutes = []; if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) { - this.runtimeDnsRoutes = this.generateDnsRoutes(); - logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.runtimeDnsRoutes) }); + this.seedDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: false }); + this.runtimeDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: true }); + logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) }); } // Combined routes for SmartProxy bootstrap (before DB routes are loaded) @@ -1338,19 +1344,20 @@ export class DcRouter { /** * Generate SmartProxy routes for DNS configuration */ - private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] { + private generateDnsRoutes(options?: { includeSocketHandler?: boolean }): plugins.smartproxy.IRouteConfig[] { if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) { return []; } - + + const includeSocketHandler = options?.includeSocketHandler !== false; const dnsRoutes: plugins.smartproxy.IRouteConfig[] = []; - + // Create routes for DNS-over-HTTPS paths const dohPaths = ['/dns-query', '/resolve']; - + // Use the first nameserver domain for DoH routes const primaryNameserver = this.options.dnsNsDomains[0]; - + for (const path of dohPaths) { const dohRoute: plugins.smartproxy.IRouteConfig = { name: `dns-over-https-${path.replace('/', '')}`, @@ -1359,18 +1366,42 @@ export class DcRouter { domains: [primaryNameserver], path: path }, - action: { - type: 'socket-handler' as any, - socketHandler: this.createDnsSocketHandler() - } as any + action: includeSocketHandler + ? { + type: 'socket-handler' as any, + socketHandler: this.createDnsSocketHandler() + } as any + : { + type: 'socket-handler' as any, + } as any }; - + dnsRoutes.push(dohRoute); } - + return dnsRoutes; } + private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined { + const routeName = storedRoute.route.name || ''; + const isDohRoute = storedRoute.origin === 'dns' + && storedRoute.route.action?.type === 'socket-handler' + && routeName.startsWith('dns-over-https-'); + + if (!isDohRoute) { + return undefined; + } + + return { + ...storedRoute.route, + action: { + ...storedRoute.route.action, + type: 'socket-handler' as any, + socketHandler: this.createDnsSocketHandler(), + } as any, + }; + } + /** * Check if a domain matches a pattern (including wildcard support) * @param domain The domain to check @@ -1939,37 +1970,20 @@ export class DcRouter { for (const domainConfig of internalDnsDomains) { const domain = domainConfig.domain; const ttl = domainConfig.dns?.internal?.ttl || 3600; - const mxPriority = domainConfig.dns?.internal?.mxPriority || 10; - - // MX record - points to the domain itself for email handling - records.push({ - name: domain, - type: 'MX', - value: `${mxPriority} ${domain}`, - ttl - }); - - // SPF record - using sensible defaults - const spfRecord = 'v=spf1 a mx ~all'; - records.push({ - name: domain, - type: 'TXT', - value: spfRecord, - ttl - }); - - // DMARC record - using sensible defaults - const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring - const dmarcEmail = `dmarc@${domain}`; - records.push({ - name: `_dmarc.${domain}`, - type: 'TXT', - value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`, - ttl - }); - - // Note: DKIM records will be generated later when DKIM keys are available - // They require the DKIMCreator which is part of the email server + const requiredRecords = buildEmailDnsRecords({ + domain, + hostname: this.options.emailConfig.hostname, + mxPriority: domainConfig.dns?.internal?.mxPriority, + }).filter((record) => !record.name.includes('._domainkey.')); + + for (const record of requiredRecords) { + records.push({ + name: record.name, + type: record.type, + value: record.value, + ttl, + }); + } } logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`); diff --git a/ts/config/classes.route-config-manager.ts b/ts/config/classes.route-config-manager.ts index 3189787..5839572 100644 --- a/ts/config/classes.route-config-manager.ts +++ b/ts/config/classes.route-config-manager.ts @@ -14,6 +14,11 @@ import type { ReferenceResolver } from './classes.reference-resolver.js'; /** An IP allow entry: plain IP/CIDR or domain-scoped. */ export type TIpAllowEntry = string | { ip: string; domains: string[] }; +export interface IRouteMutationResult { + success: boolean; + message?: string; +} + /** * Simple async mutex β€” serializes concurrent applyRoutes() calls so the Rust engine * never receives rapid overlapping route updates that can churn UDP/QUIC listeners. @@ -56,6 +61,7 @@ export class RouteConfigManager { private referenceResolver?: ReferenceResolver, private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void, private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[], + private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined, ) {} /** Expose routes map for reference resolution lookups. */ @@ -63,6 +69,10 @@ export class RouteConfigManager { return this.routes; } + public getRoute(id: string): IRoute | undefined { + return this.routes.get(id); + } + /** * Load persisted routes, seed serializable config/email/dns routes, * compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy. @@ -94,6 +104,7 @@ export class RouteConfigManager { id: route.id, enabled: route.enabled, origin: route.origin, + systemKey: route.systemKey, createdAt: route.createdAt, updatedAt: route.updatedAt, metadata: route.metadata, @@ -153,9 +164,21 @@ export class RouteConfigManager { enabled?: boolean; metadata?: Partial; }, - ): Promise { + ): Promise { const stored = this.routes.get(id); - if (!stored) return false; + if (!stored) { + return { success: false, message: 'Route not found' }; + } + + const isToggleOnlyPatch = patch.enabled !== undefined + && patch.route === undefined + && patch.metadata === undefined; + if (stored.origin !== 'api' && !isToggleOnlyPatch) { + return { + success: false, + message: 'System routes are managed by the system and can only be toggled', + }; + } if (patch.route) { const mergedAction = patch.route.action @@ -189,19 +212,29 @@ export class RouteConfigManager { await this.persistRoute(stored); await this.applyRoutes(); - return true; + return { success: true }; } - public async deleteRoute(id: string): Promise { - if (!this.routes.has(id)) return false; + public async deleteRoute(id: string): Promise { + const stored = this.routes.get(id); + if (!stored) { + return { success: false, message: 'Route not found' }; + } + if (stored.origin !== 'api') { + return { + success: false, + message: 'System routes are managed by the system and cannot be deleted', + }; + } + this.routes.delete(id); const doc = await RouteDoc.findById(id); if (doc) await doc.delete(); await this.applyRoutes(); - return true; + return { success: true }; } - public async toggleRoute(id: string, enabled: boolean): Promise { + public async toggleRoute(id: string, enabled: boolean): Promise { return this.updateRoute(id, { enabled }); } @@ -217,29 +250,28 @@ export class RouteConfigManager { seedRoutes: IDcRouterRouteConfig[], origin: 'config' | 'email' | 'dns', ): Promise { - if (seedRoutes.length === 0) return; - + const seedSystemKeys = new Set(); const seedNames = new Set(); let seeded = 0; let updated = 0; for (const route of seedRoutes) { const name = route.name || ''; - seedNames.add(name); - - // Check if a route with this name+origin already exists in memory - let existingId: string | undefined; - for (const [id, r] of this.routes) { - if (r.origin === origin && r.route.name === name) { - existingId = id; - break; - } + if (name) { + seedNames.add(name); } + const systemKey = this.buildSystemRouteKey(origin, route); + if (systemKey) { + seedSystemKeys.add(systemKey); + } + + const existingId = this.findExistingSeedRouteId(origin, route, systemKey); if (existingId) { // Update route config but preserve enabled state const existing = this.routes.get(existingId)!; existing.route = route; + existing.systemKey = systemKey; existing.updatedAt = Date.now(); await this.persistRoute(existing); updated++; @@ -255,6 +287,7 @@ export class RouteConfigManager { updatedAt: now, createdBy: 'system', origin, + systemKey, }; this.routes.set(id, newRoute); await this.persistRoute(newRoute); @@ -265,7 +298,12 @@ export class RouteConfigManager { // Delete stale routes: same origin but name not in current seed set const staleIds: string[] = []; for (const [id, r] of this.routes) { - if (r.origin === origin && !seedNames.has(r.route.name || '')) { + if (r.origin !== origin) continue; + + const routeName = r.route.name || ''; + const matchesSeedSystemKey = r.systemKey ? seedSystemKeys.has(r.systemKey) : false; + const matchesSeedName = routeName ? seedNames.has(routeName) : false; + if (!matchesSeedSystemKey && !matchesSeedName) { staleIds.push(id); } } @@ -284,9 +322,39 @@ export class RouteConfigManager { // Private: persistence // ========================================================================= + private buildSystemRouteKey( + origin: 'config' | 'email' | 'dns', + route: IDcRouterRouteConfig, + ): string | undefined { + const name = route.name?.trim(); + if (!name) return undefined; + return `${origin}:${name}`; + } + + private findExistingSeedRouteId( + origin: 'config' | 'email' | 'dns', + route: IDcRouterRouteConfig, + systemKey?: string, + ): string | undefined { + const routeName = route.name || ''; + + for (const [id, storedRoute] of this.routes) { + if (storedRoute.origin !== origin) continue; + + if (systemKey && storedRoute.systemKey === systemKey) { + return id; + } + + if (storedRoute.route.name === routeName) { + return id; + } + } + + return undefined; + } + private async loadRoutes(): Promise { const docs = await RouteDoc.findAll(); - let prunedRuntimeRoutes = 0; for (const doc of docs) { if (!doc.id) continue; @@ -299,27 +367,15 @@ export class RouteConfigManager { updatedAt: doc.updatedAt, createdBy: doc.createdBy, origin: doc.origin || 'api', + systemKey: doc.systemKey, metadata: doc.metadata, }; - if (this.isPersistedRuntimeRoute(storedRoute)) { - await doc.delete(); - prunedRuntimeRoutes++; - logger.log( - 'warn', - `Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`, - ); - continue; - } - this.routes.set(doc.id, storedRoute); } if (this.routes.size > 0) { logger.log('info', `Loaded ${this.routes.size} route(s) from database`); } - if (prunedRuntimeRoutes > 0) { - logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`); - } } private async persistRoute(stored: IRoute): Promise { @@ -330,6 +386,7 @@ export class RouteConfigManager { existingDoc.updatedAt = stored.updatedAt; existingDoc.createdBy = stored.createdBy; existingDoc.origin = stored.origin; + existingDoc.systemKey = stored.systemKey; existingDoc.metadata = stored.metadata; await existingDoc.save(); } else { @@ -341,6 +398,7 @@ export class RouteConfigManager { doc.updatedAt = stored.updatedAt; doc.createdBy = stored.createdBy; doc.origin = stored.origin; + doc.systemKey = stored.systemKey; doc.metadata = stored.metadata; await doc.save(); } @@ -411,7 +469,7 @@ export class RouteConfigManager { // Add all enabled routes with HTTP/3 and VPN augmentation for (const route of this.routes.values()) { if (route.enabled) { - enabledRoutes.push(this.prepareRouteForApply(route.route, route.id)); + enabledRoutes.push(this.prepareStoredRouteForApply(route)); } } @@ -431,6 +489,11 @@ export class RouteConfigManager { }); } + private prepareStoredRouteForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig { + const hydratedRoute = this.hydrateStoredRoute?.(storedRoute); + return this.prepareRouteForApply(hydratedRoute || storedRoute.route, storedRoute.id); + } + private prepareRouteForApply( route: plugins.smartproxy.IRouteConfig, routeId?: string, @@ -465,12 +528,4 @@ export class RouteConfigManager { }, }; } - - private isPersistedRuntimeRoute(storedRoute: IRoute): boolean { - const routeName = storedRoute.route.name || ''; - const actionType = storedRoute.route.action?.type; - - return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler') - || (storedRoute.origin === 'dns' && actionType === 'socket-handler'); - } } diff --git a/ts/db/documents/classes.route.doc.ts b/ts/db/documents/classes.route.doc.ts index 811505f..350f732 100644 --- a/ts/db/documents/classes.route.doc.ts +++ b/ts/db/documents/classes.route.doc.ts @@ -29,6 +29,9 @@ export class RouteDoc extends plugins.smartdata.SmartDataDbDoc { return await RouteDoc.getInstances({ origin }); } + + public static async findBySystemKey(systemKey: string): Promise { + return await RouteDoc.getInstance({ systemKey }); + } } diff --git a/ts/email/classes.email-domain.manager.ts b/ts/email/classes.email-domain.manager.ts index 1a2a0af..9d02fab 100644 --- a/ts/email/classes.email-domain.manager.ts +++ b/ts/email/classes.email-domain.manager.ts @@ -6,6 +6,7 @@ import { DomainDoc } from '../db/documents/classes.domain.doc.js'; import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js'; import type { DnsManager } from '../dns/manager.dns.js'; import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js'; +import { buildEmailDnsRecords } from './email-dns-records.js'; /** * EmailDomainManager β€” orchestrates email domain setup. @@ -181,34 +182,13 @@ export class EmailDomainManager { } } - const records: IEmailDnsRecord[] = [ - { - type: 'MX', - name: domain, - value: `10 ${hostname}`, - status: doc.dnsStatus.mx, - }, - { - type: 'TXT', - name: domain, - value: 'v=spf1 a mx ~all', - status: doc.dnsStatus.spf, - }, - { - type: 'TXT', - name: `${selector}._domainkey.${domain}`, - value: dkimValue, - status: doc.dnsStatus.dkim, - }, - { - type: 'TXT', - name: `_dmarc.${domain}`, - value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`, - status: doc.dnsStatus.dmarc, - }, - ]; - - return records; + return buildEmailDnsRecords({ + domain, + hostname, + selector, + dkimValue, + statuses: doc.dnsStatus, + }); } // --------------------------------------------------------------------------- diff --git a/ts/email/email-dns-records.ts b/ts/email/email-dns-records.ts new file mode 100644 index 0000000..7f73bd1 --- /dev/null +++ b/ts/email/email-dns-records.ts @@ -0,0 +1,53 @@ +import type { + IEmailDnsRecord, + TDnsRecordStatus, +} from '../../ts_interfaces/data/email-domain.js'; + +type TEmailDnsStatusKey = 'mx' | 'spf' | 'dkim' | 'dmarc'; + +export interface IBuildEmailDnsRecordsOptions { + domain: string; + hostname: string; + selector?: string; + dkimValue?: string; + mxPriority?: number; + dmarcPolicy?: string; + dmarcRua?: string; + statuses?: Partial>; +} + +export function buildEmailDnsRecords(options: IBuildEmailDnsRecordsOptions): IEmailDnsRecord[] { + const statusFor = (key: TEmailDnsStatusKey): TDnsRecordStatus => options.statuses?.[key] ?? 'unchecked'; + const selector = options.selector || 'default'; + const records: IEmailDnsRecord[] = [ + { + type: 'MX', + name: options.domain, + value: `${options.mxPriority ?? 10} ${options.hostname}`, + status: statusFor('mx'), + }, + { + type: 'TXT', + name: options.domain, + value: 'v=spf1 a mx ~all', + status: statusFor('spf'), + }, + { + type: 'TXT', + name: `_dmarc.${options.domain}`, + value: `v=DMARC1; p=${options.dmarcPolicy ?? 'none'}; rua=mailto:${options.dmarcRua ?? `dmarc@${options.domain}`}`, + status: statusFor('dmarc'), + }, + ]; + + if (options.dkimValue) { + records.splice(2, 0, { + type: 'TXT', + name: `${selector}._domainkey.${options.domain}`, + value: options.dkimValue, + status: statusFor('dkim'), + }); + } + + return records; +} diff --git a/ts/email/index.ts b/ts/email/index.ts index 42f1f00..24d399c 100644 --- a/ts/email/index.ts +++ b/ts/email/index.ts @@ -1,2 +1,3 @@ export * from './classes.email-domain.manager.js'; export * from './classes.smartmta-storage-manager.js'; +export * from './email-dns-records.js'; diff --git a/ts/opsserver/handlers/route-management.handler.ts b/ts/opsserver/handlers/route-management.handler.ts index d8d9943..7a8e0aa 100644 --- a/ts/opsserver/handlers/route-management.handler.ts +++ b/ts/opsserver/handlers/route-management.handler.ts @@ -87,12 +87,12 @@ export class RouteManagementHandler { if (!manager) { return { success: false, message: 'Route management not initialized' }; } - const ok = await manager.updateRoute(dataArg.id, { + const result = await manager.updateRoute(dataArg.id, { route: dataArg.route as any, enabled: dataArg.enabled, metadata: dataArg.metadata, }); - return { success: ok, message: ok ? undefined : 'Route not found' }; + return result; }, ), ); @@ -107,8 +107,7 @@ export class RouteManagementHandler { if (!manager) { return { success: false, message: 'Route management not initialized' }; } - const ok = await manager.deleteRoute(dataArg.id); - return { success: ok, message: ok ? undefined : 'Route not found' }; + return manager.deleteRoute(dataArg.id); }, ), ); @@ -123,8 +122,7 @@ export class RouteManagementHandler { if (!manager) { return { success: false, message: 'Route management not initialized' }; } - const ok = await manager.toggleRoute(dataArg.id, dataArg.enabled); - return { success: ok, message: ok ? undefined : 'Route not found' }; + return manager.toggleRoute(dataArg.id, dataArg.enabled); }, ), ); diff --git a/ts_apiclient/readme.md b/ts_apiclient/readme.md index 25fbad5..7aeb009 100644 --- a/ts_apiclient/readme.md +++ b/ts_apiclient/readme.md @@ -1,8 +1,8 @@ # @serve.zone/dcrouter-apiclient -A typed, object-oriented API client for DcRouter with a fluent builder pattern. πŸ”§ +Typed, object-oriented API client for operating a running dcrouter instance. πŸ”§ -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. +Use this package when you want a clean TypeScript client instead of manually firing TypedRequest calls. It wraps the OpsServer API in resource managers and resource classes such as routes, certificates, tokens, edges, emails, stats, logs, config, and RADIUS. ## Issue Reporting and Security @@ -14,7 +14,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community pnpm add @serve.zone/dcrouter-apiclient ``` -Or import directly from the main package: +Or import through the main package: ```typescript import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient'; @@ -23,239 +23,113 @@ import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient'; ## Quick Start ```typescript -import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient'; +import { DcRouterApiClient } from '@serve.zone/dcrouter-apiclient'; -const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' }); +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`); +const { routes } = await client.routes.list(); +console.log(routes.map((route) => `${route.origin}:${route.name}`)); -// Check health -const { health } = await client.stats.getHealth(); -console.log(`Healthy: ${health.healthy}`); +await client.routes.build() + .setName('api-gateway') + .setMatch({ ports: 443, domains: ['api.example.com'] }) + .setAction({ type: 'forward', targets: [{ host: '127.0.0.1', port: 8080 }] }) + .save(); ``` -## Usage +## Authentication Modes -### πŸ” Authentication +| Mode | How it works | +| --- | --- | +| Admin login | Call `login(username, password)` and the client stores the returned identity for later requests | +| API token | Pass `apiToken` into the constructor for token-based automation | ```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 +## Main Managers -Routes are returned as `Route` instances with methods for update, delete, toggle, and overrides: +| Manager | Purpose | +| --- | --- | +| `client.routes` | List routes and create API-managed routes | +| `client.certificates` | Inspect and operate on certificate records | +| `client.apiTokens` | Create, list, toggle, roll, revoke API tokens | +| `client.remoteIngress` | Manage registered remote ingress edges | +| `client.stats` | Read operational metrics and health data | +| `client.config` | Read current configuration view | +| `client.logs` | Read recent logs or stream them | +| `client.emails` | List emails and trigger resend flows | +| `client.radius` | Operate on RADIUS clients, VLANs, sessions, and accounting | + +## Route Behavior + +Routes are returned as `Route` instances with: + +- `id` +- `name` +- `enabled` +- `origin` + +Important behavior: + +- API routes can be created, updated, deleted, and toggled. +- System routes can be listed and toggled, but not edited or deleted. +- A system route is any route whose `origin !== 'api'`. ```typescript -// List all routes (hardcoded + programmatic) -const { routes, warnings } = await client.routes.list(); +const { routes } = 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(); +for (const route of routes) { + if (route.origin !== 'api') { + await route.toggle(false); + } +} ``` -**Builder pattern** for creating new routes: +## Builder Example ```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' }) +const route = await client.routes.build() + .setName('internal-app') + .setMatch({ + ports: 80, + domains: ['internal.example.com'], + }) + .setAction({ + type: 'forward', + targets: [{ host: '127.0.0.1', port: 3000 }], + }) .setEnabled(true) .save(); -// Or use quick creation -const route = await client.routes.create(routeConfig); +await route.toggle(false); ``` -### πŸ”‘ 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 +## Example: Certificates and Stats ```typescript const { certificates, summary } = await client.certificates.list(); -console.log(`${summary.valid} valid, ${summary.expiring} expiring, ${summary.failed} failed`); +console.log(summary.valid, summary.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: '...', -}); +const health = await client.stats.getHealth(); +const recentLogs = await client.logs.getRecent({ level: 'error', limit: 20 }); ``` -### 🌍 Remote Ingress +## What This Package Does Not Do -```typescript -// List edges and their statuses -const edges = await client.remoteIngress.list(); -const statuses = await client.remoteIngress.getStatuses(); +- It does not start dcrouter. +- It does not embed the dashboard. +- It does not replace the request interfaces package if you only need raw types. -// 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. +Use `@serve.zone/dcrouter` to run the server, `@serve.zone/dcrouter-web` for the dashboard bundle/components, and `@serve.zone/dcrouter-interfaces` for raw API contracts. ## License and Legal Information diff --git a/ts_interfaces/data/route-management.ts b/ts_interfaces/data/route-management.ts index efbf71c..d77c027 100644 --- a/ts_interfaces/data/route-management.ts +++ b/ts_interfaces/data/route-management.ts @@ -90,6 +90,7 @@ export interface IMergedRoute { id: string; enabled: boolean; origin: 'config' | 'email' | 'dns' | 'api'; + systemKey?: string; createdAt?: number; updatedAt?: number; metadata?: IRouteMetadata; @@ -132,6 +133,7 @@ export interface IRoute { updatedAt: number; createdBy: string; origin: 'config' | 'email' | 'dns' | 'api'; + systemKey?: string; metadata?: IRouteMetadata; } diff --git a/ts_interfaces/readme.md b/ts_interfaces/readme.md index 68d701d..66436e0 100644 --- a/ts_interfaces/readme.md +++ b/ts_interfaces/readme.md @@ -1,8 +1,8 @@ # @serve.zone/dcrouter-interfaces -TypeScript interfaces and type definitions for the DcRouter OpsServer API. πŸ“‘ +Shared TypeScript request and data interfaces for dcrouter's OpsServer API. πŸ“‘ -This module provides strongly-typed interfaces for communicating with the DcRouter OpsServer via [TypedRequest](https://code.foss.global/api.global/typedrequest). Use these interfaces for type-safe API interactions in your frontend applications or integration code. +This package is the contract layer for typed clients, frontend code, tests, or automation that talks to a running dcrouter instance through TypedRequest. ## Issue Reporting and Security @@ -14,320 +14,79 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community pnpm add @serve.zone/dcrouter-interfaces ``` -Or import directly from the main package: +Or consume the same interfaces through the main package: ```typescript import { data, requests } from '@serve.zone/dcrouter/interfaces'; ``` -## Usage +## What It Exports + +The package exposes two namespaces from `index.ts`: + +| Export | Purpose | +| --- | --- | +| `data` | Shared runtime-shaped types such as route data, auth identity, stats, domains, certificates, VPN, DNS, and email-domain data | +| `requests` | TypedRequest request and response contracts for every OpsServer endpoint | + +## Example ```typescript +import * as typedrequest from '@api.global/typedrequest'; import { data, requests } from '@serve.zone/dcrouter-interfaces'; -// Use data interfaces for type definitions const identity: data.IIdentity = { - jwt: 'your-jwt-token', - userId: 'user-123', - name: 'Admin User', - expiresAt: Date.now() + 3600000, - role: 'admin' + jwt: 'jwt-token', + userId: 'admin-1', + name: 'Admin', + expiresAt: Date.now() + 60_000, + role: 'admin', }; -// Use request interfaces for API calls -import * as typedrequest from '@api.global/typedrequest'; - -const statsClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'getServerStatistics' +const request = new typedrequest.TypedRequest( + 'https://dcrouter.example.com/typedrequest', + 'getMergedRoutes', ); -const stats = await statsClient.fire({ - identity, - includeHistory: true, - timeRange: '24h' -}); -``` +const response = await request.fire({ identity }); -## Module Structure - -### Data Interfaces (`data`) - -Core data types used throughout the DcRouter system: - -#### `IIdentity` -Authentication identity for API requests: -```typescript -interface IIdentity { - jwt: string; // JWT token - userId: string; // Unique user ID - name: string; // Display name - expiresAt: number; // Token expiration timestamp - role?: string; // User role (e.g., 'admin') - type?: string; // Identity type +for (const route of response.routes) { + console.log(route.id, route.origin, route.systemKey, route.enabled); } ``` -#### Statistics Interfaces -| Interface | Description | -|-----------|-------------| -| `IServerStats` | Uptime, memory, CPU, connection counts | -| `IEmailStats` | Sent/received/bounced/queued/failed, delivery & bounce rates | -| `IDnsStats` | Total queries, cache hits/misses, query types | -| `IRateLimitInfo` | Domain rate limit status (current rate, limit, remaining) | -| `ISecurityMetrics` | Blocked IPs, spam/malware/phishing counts | -| `IConnectionInfo` | Connection ID, remote address, protocol, state, bytes | -| `IQueueStatus` | Queue name, size, processing/failed/retrying counts | -| `IHealthStatus` | Healthy flag, uptime, per-service status map | -| `INetworkMetrics` | Bandwidth, connection counts, top endpoints | -| `IRadiusStats` | Running, uptime, auth requests/accepts/rejects, sessions, data transfer | -| `IVpnStats` | Running, subnet, registered/connected clients, WireGuard port | -| `ILogEntry` | Timestamp, level, category, message, metadata | +## API Domains Covered -#### 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` | +| Domain | Examples | +| --- | --- | +| Auth | admin login, logout, identity verification | +| Routes | merged routes, create, update, delete, toggle | +| Access | API tokens, source profiles, target profiles, network targets | +| DNS and domains | providers, domains, DNS records | +| Certificates | overview, reprovision, import, export, delete, ACME config | +| Email | email operations, email domains | +| Remote ingress | edge registrations, status, connection tokens | +| VPN | clients, status, telemetry, lifecycle | +| RADIUS | clients, VLANs, sessions, accounting | +| Observability | stats, logs, health, configuration | -#### Security & Reference Interfaces -| Interface | Description | -|-----------|-------------| -| `ISecurityProfile` | Reusable security config: id, name, description, security (ipAllowList, ipBlockList, maxConnections, rateLimit, etc.), extendsProfiles | -| `INetworkTarget` | Reusable host:port destination: id, name, description, host (string or string[]), port | -| `IRouteMetadata` | Route-to-reference links: securityProfileRef, networkTargetRef, snapshot names, lastResolvedAt | +## Notable Data Types -#### Remote Ingress Interfaces -| Interface | Description | -|-----------|-------------| -| `IRemoteIngress` | Edge registration: id, name, secret, listenPorts, enabled, autoDerivePorts, tags | -| `IRemoteIngressStatus` | Runtime status: connected, publicIp, activeTunnels, lastHeartbeat | -| `IRouteRemoteIngress` | Route-level config: enabled flag and optional edgeFilter | -| `IDcRouterRouteConfig` | Extended SmartProxy route config with optional `remoteIngress` and `vpn` properties | -| `IRouteVpn` | Route-level VPN config: `enabled`/`mandatory` flags and optional `allowedServerDefinedClientTags` | +| Type | Description | +| --- | --- | +| `data.IMergedRoute` | Route entry returned by route management, including `origin`, `enabled`, and optional `systemKey` | +| `data.IDcRouterRouteConfig` | dcrouter-flavored route config used across the stack | +| `data.IRouteMetadata` | Reference metadata connecting routes to source profiles or network targets | +| `data.IIdentity` | Admin identity used for authenticated requests | +| `data.IApiTokenInfo` | Public token metadata without the secret | -#### VPN Interfaces -| Interface | Description | -|-----------|-------------| -| `IVpnClient` | Client registration: clientId, enabled, serverDefinedClientTags, description, assignedIp, timestamps | -| `IVpnServerStatus` | Server status: running, subnet, wgListenPort, publicKeys, client counts | -| `IVpnClientTelemetry` | Per-client metrics: bytes sent/received, packets dropped, keepalives, rate limits | +## When To Use This Package -### Request Interfaces (`requests`) +- Use it in custom dashboards or CLIs that call TypedRequest directly. +- Use it in tests that need strongly typed request payloads or response assertions. +- Use it when you want the API contract without pulling in the OO client. -TypedRequest interfaces for the OpsServer API, organized by domain: - -#### πŸ” Authentication -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_AdminLoginWithUsernameAndPassword` | `adminLoginWithUsernameAndPassword` | Authenticate as admin | -| `IReq_AdminLogout` | `adminLogout` | End admin session | -| `IReq_VerifyIdentity` | `verifyIdentity` | Verify JWT token validity | - -#### πŸ“Š Statistics -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetServerStatistics` | `getServerStatistics` | Overall server stats | -| `IReq_GetEmailStatistics` | `getEmailStatistics` | Email throughput metrics | -| `IReq_GetDnsStatistics` | `getDnsStatistics` | DNS query stats | -| `IReq_GetRateLimitStatus` | `getRateLimitStatus` | Rate limit status | -| `IReq_GetSecurityMetrics` | `getSecurityMetrics` | Security event metrics | -| `IReq_GetActiveConnections` | `getActiveConnections` | Active connection list | -| `IReq_GetQueueStatus` | `getQueueStatus` | Email queue status | -| `IReq_GetHealthStatus` | `getHealthStatus` | System health check | -| `IReq_GetNetworkStats` | `getNetworkStats` | Network throughput and connection analytics | -| `IReq_GetCombinedMetrics` | `getCombinedMetrics` | All metrics in one request (server, email, DNS, security, network, RADIUS, VPN) | - -#### βš™οΈ Configuration -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetConfiguration` | `getConfiguration` | Current config (read-only) | - -#### πŸ“œ Logs -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetRecentLogs` | `getLogs` | Retrieve system logs | -| `IReq_GetLogStream` | `getLogStream` | Stream live logs | - -#### πŸ“§ Email Operations -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetAllEmails` | `getAllEmails` | List all emails | -| `IReq_GetEmailDetail` | `getEmailDetail` | Full detail for a specific email | -| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email | - -#### πŸ›£οΈ 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 | -|-----------|--------|-------------| -| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status | -| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) | -| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) | -| `IReq_ImportCertificate` | `importCertificate` | Import a certificate | -| `IReq_ExportCertificate` | `exportCertificate` | Export a certificate | -| `IReq_DeleteCertificate` | `deleteCertificate` | Delete a certificate | - -#### Certificate Types -```typescript -type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown'; -type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none'; - -interface ICertificateInfo { - domain: string; - routeNames: string[]; - status: TCertificateStatus; - source: TCertificateSource; - tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough'; - expiryDate?: string; - issuer?: string; - issuedAt?: string; - error?: string; - canReprovision: boolean; - backoffInfo?: { - failures: number; - retryAfter?: string; - lastError?: string; - }; -} -``` - -#### 🌍 Remote Ingress -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_CreateRemoteIngress` | `createRemoteIngress` | Register a new edge node | -| `IReq_DeleteRemoteIngress` | `deleteRemoteIngress` | Remove an edge registration | -| `IReq_UpdateRemoteIngress` | `updateRemoteIngress` | Update edge settings | -| `IReq_RegenerateRemoteIngressSecret` | `regenerateRemoteIngressSecret` | Issue a new secret | -| `IReq_GetRemoteIngresses` | `getRemoteIngresses` | List all edge registrations | -| `IReq_GetRemoteIngressStatus` | `getRemoteIngressStatus` | Runtime status of all edges | -| `IReq_GetRemoteIngressConnectionToken` | `getRemoteIngressConnectionToken` | Generate a connection token | - -#### πŸ” VPN -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetVpnClients` | `getVpnClients` | List all registered VPN clients | -| `IReq_GetVpnStatus` | `getVpnStatus` | VPN server status | -| `IReq_CreateVpnClient` | `createVpnClient` | Create a new VPN client (returns WireGuard config) | -| `IReq_DeleteVpnClient` | `deleteVpnClient` | Remove a VPN client | -| `IReq_EnableVpnClient` | `enableVpnClient` | Enable a disabled client | -| `IReq_DisableVpnClient` | `disableVpnClient` | Disable a client | -| `IReq_RotateVpnClientKey` | `rotateVpnClientKey` | Generate new keys for a client | -| `IReq_ExportVpnClientConfig` | `exportVpnClientConfig` | Export WireGuard or SmartVPN config | -| `IReq_GetVpnClientTelemetry` | `getVpnClientTelemetry` | Per-client traffic metrics | - -#### πŸ“‘ RADIUS -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetRadiusClients` | `getRadiusClients` | List NAS clients | -| `IReq_SetRadiusClient` | `setRadiusClient` | Add/update a NAS client | -| `IReq_RemoveRadiusClient` | `removeRadiusClient` | Remove a NAS client | -| `IReq_GetVlanMappings` | `getVlanMappings` | List VLAN mappings | -| `IReq_SetVlanMapping` | `setVlanMapping` | Add/update VLAN mapping | -| `IReq_RemoveVlanMapping` | `removeVlanMapping` | Remove VLAN mapping | -| `IReq_TestVlanAssignment` | `testVlanAssignment` | Test what VLAN a MAC gets | -| `IReq_GetRadiusSessions` | `getRadiusSessions` | List active sessions | -| `IReq_DisconnectRadiusSession` | `disconnectRadiusSession` | Force disconnect | -| `IReq_GetRadiusStatistics` | `getRadiusStatistics` | RADIUS stats | -| `IReq_GetRadiusAccountingSummary` | `getRadiusAccountingSummary` | Accounting summary | - -#### πŸ›‘οΈ Security Profiles -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetSecurityProfiles` | `getSecurityProfiles` | List all security profiles | -| `IReq_GetSecurityProfile` | `getSecurityProfile` | Get a single profile by ID | -| `IReq_CreateSecurityProfile` | `createSecurityProfile` | Create a reusable security profile | -| `IReq_UpdateSecurityProfile` | `updateSecurityProfile` | Update a profile (propagates to routes) | -| `IReq_DeleteSecurityProfile` | `deleteSecurityProfile` | Delete a profile (with optional force) | -| `IReq_GetSecurityProfileUsage` | `getSecurityProfileUsage` | Get routes referencing a profile | - -#### 🎯 Network Targets -| Interface | Method | Description | -|-----------|--------|-------------| -| `IReq_GetNetworkTargets` | `getNetworkTargets` | List all network targets | -| `IReq_GetNetworkTarget` | `getNetworkTarget` | Get a single target by ID | -| `IReq_CreateNetworkTarget` | `createNetworkTarget` | Create a reusable host:port target | -| `IReq_UpdateNetworkTarget` | `updateNetworkTarget` | Update a target (propagates to routes) | -| `IReq_DeleteNetworkTarget` | `deleteNetworkTarget` | Delete a target (with optional force) | -| `IReq_GetNetworkTargetUsage` | `getNetworkTargetUsage` | Get routes referencing a target | - -## 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'; - -// 1. Login -const loginClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'adminLoginWithUsernameAndPassword' -); - -const loginResponse = await loginClient.fire({ - username: 'admin', - password: 'your-password' -}); -const identity = loginResponse.identity; - -// 2. Fetch combined metrics -const metricsClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'getCombinedMetrics' -); - -const metrics = await metricsClient.fire({ identity }); -console.log('Server:', metrics.metrics.server); -console.log('Email:', metrics.metrics.email); - -// 3. Check certificate status -const certClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'getCertificateOverview' -); - -const certs = await certClient.fire({ identity }); -console.log(`Certificates: ${certs.summary.valid} valid, ${certs.summary.failed} failed`); - -// 4. List remote ingress edges -const edgesClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'getRemoteIngresses' -); - -const edges = await edgesClient.fire({ identity }); -console.log('Registered edges:', edges.edges.length); - -// 5. Generate a connection token for an edge -const tokenClient = new typedrequest.TypedRequest( - 'https://your-dcrouter:3000/typedrequest', - 'getRemoteIngressConnectionToken' -); - -const tokenResponse = await tokenClient.fire({ identity, edgeId: edges.edges[0].id }); -console.log('Connection token:', tokenResponse.token); -``` +If you want a higher-level client with managers and resource classes, use `@serve.zone/dcrouter-apiclient` instead. ## License and Legal Information diff --git a/ts_migrations/index.ts b/ts_migrations/index.ts index d04649c..b228e29 100644 --- a/ts_migrations/index.ts +++ b/ts_migrations/index.ts @@ -45,6 +45,33 @@ async function migrateTargetProfileTargetHosts(ctx: { ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`); } +async function backfillSystemRouteKeys(ctx: { + mongo?: { collection: (name: string) => any }; + log: { log: (level: 'info', message: string) => void }; +}): Promise { + const collection = ctx.mongo!.collection('RouteDoc'); + const cursor = collection.find({ + origin: { $in: ['config', 'email', 'dns'] }, + systemKey: { $exists: false }, + 'route.name': { $type: 'string' }, + }); + let migrated = 0; + + for await (const doc of cursor) { + const origin = typeof (doc as any).origin === 'string' ? (doc as any).origin : undefined; + const routeName = typeof (doc as any).route?.name === 'string' ? (doc as any).route.name.trim() : ''; + if (!origin || !routeName) continue; + + await collection.updateOne( + { _id: (doc as any)._id }, + { $set: { systemKey: `${origin}:${routeName}` } }, + ); + migrated++; + } + + ctx.log.log('info', `backfill-system-route-keys: migrated ${migrated} route(s)`); +} + /** * Create a configured SmartMigration runner with all dcrouter migration steps registered. * @@ -134,6 +161,12 @@ export async function createMigrationRunner( .description('Repair TargetProfileDoc.targets hostβ†’ip migration for already-upgraded installs') .up(async (ctx) => { await migrateTargetProfileTargetHosts(ctx); + }) + .step('backfill-system-route-keys') + .from('13.17.4').to('13.18.0') + .description('Backfill RouteDoc.systemKey for persisted config/email/dns routes') + .up(async (ctx) => { + await backfillSystemRouteKeys(ctx); }); return migration; diff --git a/ts_migrations/readme.md b/ts_migrations/readme.md new file mode 100644 index 0000000..efe651c --- /dev/null +++ b/ts_migrations/readme.md @@ -0,0 +1,67 @@ +# @serve.zone/dcrouter-migrations + +Migration runner package for dcrouter's smartdata-backed persistence layer. 🧱 + +This package provides the startup migration chain that upgrades dcrouter data across releases before the application reads from the database. + +## 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. + +## What It Exports + +| Export | Purpose | +| --- | --- | +| `createMigrationRunner(db, targetVersion)` | Builds the dcrouter SmartMigration runner for the current application version | +| `IMigrationRunner` | Small interface describing the runner's `run()` method | +| `IMigrationRunResult` | Logged result shape used after execution | + +## Usage + +```typescript +import { createMigrationRunner } from '@serve.zone/dcrouter-migrations'; + +const migration = await createMigrationRunner(db, '13.18.0'); +const result = await migration.run(); + +console.log(result.currentVersionBefore, result.currentVersionAfter); +``` + +## What These Migrations Handle + +The migration chain currently covers dcrouter-specific storage transitions such as: + +- target profile target field renames +- domain and DNS record source renames +- route collection unification into `RouteDoc` +- persisted route metadata backfills such as `origin` and `systemKey` + +## Important Behavior + +- fresh installs are stamped directly to the current target version +- migration steps are registered in strict version order +- migrations run before services load DB-backed state +- route-related migrations use smartdata collection names exactly as declared in code + +If you are embedding dcrouter's DB layer outside the main runtime, run this package before any feature code assumes the latest schema. + +## 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. diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index f9f1930..74c6bb3 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.18.0', + version: '13.19.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index d3c2d64..a799d15 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -2150,7 +2150,7 @@ export const updateRouteAction = routeManagementStatePart.createAction<{ interfaces.requests.IReq_UpdateRoute >('/typedrequest', 'updateRoute'); - await request.fire({ + const response = await request.fire({ identity: context.identity!, id: dataArg.id, route: dataArg.route, @@ -2158,6 +2158,10 @@ export const updateRouteAction = routeManagementStatePart.createAction<{ metadata: dataArg.metadata, }); + if (!response.success) { + throw new Error(response.message || 'Failed to update route'); + } + return await actionContext!.dispatch(fetchMergedRoutesAction, null); } catch (error: unknown) { return { @@ -2177,11 +2181,15 @@ export const deleteRouteAction = routeManagementStatePart.createAction( interfaces.requests.IReq_DeleteRoute >('/typedrequest', 'deleteRoute'); - await request.fire({ + const response = await request.fire({ identity: context.identity!, id: routeId, }); + if (!response.success) { + throw new Error(response.message || 'Failed to delete route'); + } + return await actionContext!.dispatch(fetchMergedRoutesAction, null); } catch (error: unknown) { return { @@ -2204,12 +2212,16 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{ interfaces.requests.IReq_ToggleRoute >('/typedrequest', 'toggleRoute'); - await request.fire({ + const response = await request.fire({ identity: context.identity!, id: dataArg.id, enabled: dataArg.enabled, }); + if (!response.success) { + throw new Error(response.message || 'Failed to toggle route'); + } + return await actionContext!.dispatch(fetchMergedRoutesAction, null); } catch (error: unknown) { return { @@ -2765,4 +2777,4 @@ startAutoRefresh(); // Connect TypedSocket if already logged in (e.g., persistent session) if (loginStatePart.getState()!.isLoggedIn) { connectSocket(); -} \ No newline at end of file +} diff --git a/ts_web/elements/network/ops-view-routes.ts b/ts_web/elements/network/ops-view-routes.ts index 05c1477..f4549f2 100644 --- a/ts_web/elements/network/ops-view-routes.ts +++ b/ts_web/elements/network/ops-view-routes.ts @@ -272,15 +272,13 @@ export class OpsViewRoutes extends DeesElement { const clickedRoute = e.detail; if (!clickedRoute) return; - // Find the corresponding merged route - const merged = this.routeState.mergedRoutes.find( - (mr) => mr.route.name === clickedRoute.name, - ); + const merged = this.findMergedRoute(clickedRoute); if (!merged) return; const { DeesModal } = await import('@design.estate/dees-catalog'); const meta = merged.metadata; + const isSystemManaged = this.isSystemManagedRoute(merged); await DeesModal.createAndShow({ heading: `Route: ${merged.route.name}`, content: html` @@ -288,6 +286,7 @@ export class OpsViewRoutes extends DeesElement {

Origin: ${merged.origin}

Status: ${merged.enabled ? 'Enabled' : 'Disabled'}

ID: ${merged.id}

+ ${isSystemManaged ? html`

This route is system-managed. Change its source config to modify it directly.

` : ''} ${meta?.sourceProfileName ? html`

Source Profile: ${meta.sourceProfileName}

` : ''} ${meta?.networkTargetName ? html`

Network Target: ${meta.networkTargetName}

` : ''} @@ -304,25 +303,29 @@ export class OpsViewRoutes extends DeesElement { await modalArg.destroy(); }, }, - { - name: 'Edit', - iconName: 'lucide:pencil', - action: async (modalArg: any) => { - await modalArg.destroy(); - this.showEditRouteDialog(merged); - }, - }, - { - name: 'Delete', - iconName: 'lucide:trash-2', - action: async (modalArg: any) => { - await appstate.routeManagementStatePart.dispatchAction( - appstate.deleteRouteAction, - merged.id, - ); - await modalArg.destroy(); - }, - }, + ...(!isSystemManaged + ? [ + { + name: 'Edit', + iconName: 'lucide:pencil', + action: async (modalArg: any) => { + await modalArg.destroy(); + this.showEditRouteDialog(merged); + }, + }, + { + name: 'Delete', + iconName: 'lucide:trash-2', + action: async (modalArg: any) => { + await appstate.routeManagementStatePart.dispatchAction( + appstate.deleteRouteAction, + merged.id, + ); + await modalArg.destroy(); + }, + }, + ] + : []), { name: 'Close', iconName: 'lucide:x', @@ -336,10 +339,9 @@ export class OpsViewRoutes extends DeesElement { const clickedRoute = e.detail; if (!clickedRoute) return; - const merged = this.routeState.mergedRoutes.find( - (mr) => mr.route.name === clickedRoute.name, - ); + const merged = this.findMergedRoute(clickedRoute); if (!merged) return; + if (this.isSystemManagedRoute(merged)) return; this.showEditRouteDialog(merged); } @@ -348,10 +350,9 @@ export class OpsViewRoutes extends DeesElement { const clickedRoute = e.detail; if (!clickedRoute) return; - const merged = this.routeState.mergedRoutes.find( - (mr) => mr.route.name === clickedRoute.name, - ); + const merged = this.findMergedRoute(clickedRoute); if (!merged) return; + if (this.isSystemManagedRoute(merged)) return; const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ @@ -675,6 +676,23 @@ export class OpsViewRoutes extends DeesElement { appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); } + private findMergedRoute(clickedRoute: { id?: string; name?: string }): interfaces.data.IMergedRoute | undefined { + if (clickedRoute.id) { + const routeById = this.routeState.mergedRoutes.find((mr) => mr.id === clickedRoute.id); + if (routeById) return routeById; + } + + if (clickedRoute.name) { + return this.routeState.mergedRoutes.find((mr) => mr.route.name === clickedRoute.name); + } + + return undefined; + } + + private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean { + return merged.origin !== 'api'; + } + async firstUpdated() { await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null); diff --git a/ts_web/readme.md b/ts_web/readme.md index 791c802..f976886 100644 --- a/ts_web/readme.md +++ b/ts_web/readme.md @@ -1,273 +1,72 @@ # @serve.zone/dcrouter-web -Web-based Operations Dashboard for DcRouter. πŸ–₯️ +Browser UI package for dcrouter's operations dashboard. πŸ–₯️ -A modern, reactive web application for monitoring and managing your DcRouter instance in real-time. Built with web components using [@design.estate/dees-element](https://code.foss.global/design.estate/dees-element) and [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog). +This package contains the browser entrypoint, app state, router, and web components that power the Ops dashboard served by dcrouter. ## 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. -## Features +## What Is In Here -### πŸ” Secure Authentication -- JWT-based login with persistent sessions (IndexedDB) -- Automatic session expiry detection and cleanup -- Secure username/password authentication +| Path | Purpose | +| --- | --- | +| `index.ts` | Browser entrypoint that initializes routing and renders `` | +| `appstate.ts` | Central reactive state and action definitions | +| `router.ts` | URL-based dashboard routing | +| `elements/` | Dashboard views and reusable UI pieces | -### πŸ“Š Overview Dashboard -- Real-time server statistics (CPU, memory, uptime) -- Active connection counts and email throughput -- DNS query metrics and RADIUS session tracking -- Auto-refreshing with configurable intervals +## Main Views -### 🌐 Network View -- Active connection monitoring with real-time data from SmartProxy -- Top connected IPs with connection counts and percentages -- Throughput rates (inbound/outbound in kbit/s, Mbit/s, Gbit/s) -- Traffic chart with selectable time ranges +The dashboard currently includes views for: -### πŸ“§ Email Management -- **Queued** β€” Emails pending delivery with queue position -- **Sent** β€” Successfully delivered emails with timestamps -- **Failed** β€” Failed emails with resend capability -- **Security** β€” Security incidents from email processing -- Bounce record management and suppression list controls +- overview and configuration +- network activity and route management +- source profiles, target profiles, and network targets +- email activity and email domains +- DNS providers, domains, DNS records, and certificates +- API tokens and users +- VPN, remote ingress, logs, and security views -### πŸ” Certificate Management -- Domain-centric certificate overview with status indicators -- Certificate source tracking (ACME, provision function, static) -- Expiry date monitoring and alerts -- Per-domain backoff status for failed provisions -- One-click reprovisioning per domain -- Certificate import, export, and deletion +## Route Management UX -### 🌍 Remote Ingress Management -- Edge node registration with name, ports, and tags -- Real-time connection status (connected/disconnected/disabled) -- Public IP and active tunnel count per edge -- Auto-derived port display with manual/derived breakdown -- **Connection token generation** β€” one-click "Copy Token" for easy edge provisioning -- Enable/disable, edit, secret regeneration, and delete actions +The web UI reflects dcrouter's current route ownership model: -### πŸ” VPN Management -- VPN server status with forwarding mode, subnet, and WireGuard port -- Client registration table with create, enable/disable, and delete actions -- WireGuard config download, clipboard copy, and **QR code display** on client creation -- QR code export for existing clients β€” scan with WireGuard mobile app (iOS/Android) -- Per-client telemetry (bytes sent/received, keepalives) -- Server public key display for manual client configuration +- system routes are shown separately from user routes +- system routes are visible and toggleable +- system routes are not directly editable or deletable +- API routes are fully managed through the route-management forms -### πŸ“œ Log Viewer -- Real-time log streaming -- Filter by log level (error, warning, info, debug) -- Search and time-range selection +## How It Talks To dcrouter -### πŸ›£οΈ 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 +The frontend uses TypedRequest and shared interfaces from `@serve.zone/dcrouter-interfaces`. -### πŸ›‘οΈ Security Profiles & Network Targets -- Create, edit, and delete reusable security profiles (IP allow/block lists, rate limits, max connections) -- Create, edit, and delete reusable network targets (host:port destinations) -- In-row and context menu actions for quick editing -- Changes propagate automatically to all referencing routes +State actions in `appstate.ts` fetch and mutate: -### βš™οΈ Configuration -- Read-only display of current system configuration -- Status badges for boolean values (enabled/disabled) -- Array values displayed as pills with counts -- Section icons and formatted byte/time values +- stats and health +- logs +- routes and tokens +- certificates and ACME config +- DNS providers, domains, and records +- email domains and email operations +- VPN, remote ingress, and RADIUS data -### πŸ›‘οΈ Security Dashboard -- IP reputation monitoring -- Rate limit status across domains -- Blocked connection tracking -- Security event timeline +## Development Notes -## Architecture - -### Technology Stack - -| Layer | Package | Purpose | -|-------|---------|---------| -| **Components** | `@design.estate/dees-element` | Web component framework (lit-element based) | -| **UI Kit** | `@design.estate/dees-catalog` | Pre-built components (tables, charts, forms, app shell) | -| **State** | `@push.rocks/smartstate` | Reactive state management with persistent/soft modes | -| **Routing** | Client-side router | URL-synchronized view navigation | -| **API** | `@api.global/typedrequest` | Type-safe communication with OpsServer | -| **Types** | `@serve.zone/dcrouter-interfaces` | Shared TypedRequest interface definitions | - -### Component Structure - -``` -ts_web/ -β”œβ”€β”€ index.ts # Entry point β€” renders -β”œβ”€β”€ appstate.ts # State management (all state parts + actions) -β”œβ”€β”€ router.ts # Client-side routing (AppRouter) -β”œβ”€β”€ plugins.ts # Dependency imports -└── elements/ - β”œβ”€β”€ ops-dashboard.ts # Main app shell - β”œβ”€β”€ ops-view-overview.ts # Overview statistics - β”œβ”€β”€ ops-view-network.ts # Network monitoring - β”œβ”€β”€ ops-view-emails.ts # Email queue management - β”œβ”€β”€ ops-view-certificates.ts # Certificate overview & reprovisioning - β”œβ”€β”€ ops-view-remoteingress.ts # Remote ingress edge management - β”œβ”€β”€ ops-view-vpn.ts # VPN client 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/ - β”œβ”€β”€ css.ts # Shared styles - └── ops-sectionheading.ts # Section heading component -``` - -### State Management - -The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled actions with `autoPause: 'visibility'`, and batched updates: - -| State Part | Mode | Description | -|-----------|------|-------------| -| `loginStatePart` | Persistent (IndexedDB) | JWT identity and login status | -| `statsStatePart` | Soft (memory) | Server, email, DNS, security, RADIUS, VPN metrics | -| `configStatePart` | Soft | Current system configuration | -| `uiStatePart` | Soft | Active view, sidebar, auto-refresh, theme | -| `logStatePart` | Soft | Recent logs, streaming status, filters | -| `networkStatePart` | Soft | Connections, IPs, throughput rates | -| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list | -| `certificateStatePart` | Soft | Certificate list, summary, loading state | -| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret | -| `vpnStatePart` | Soft | VPN clients, server status, new client config | - -### Tab Visibility Optimization - -The dashboard automatically pauses all background activity when the browser tab is hidden and resumes when visible: - -- **Auto-refresh polling** uses `createScheduledAction` with `autoPause: 'visibility'` β€” stops HTTP requests while the tab is sleeping -- **In-flight guard** prevents concurrent refresh requests from piling up -- **WebSocket connection** disconnects when hidden and reconnects when visible, preventing log entry accumulation -- **Network traffic timer** pauses chart updates when the tab is backgrounded -- **Log entry batching** β€” incoming WebSocket log pushes are buffered and flushed once per animation frame to avoid per-entry re-renders - -### Actions - -```typescript -// Authentication -loginAction(username, password) // JWT login -logoutAction() // Clear session - -// Data fetching (auto-refresh compatible) -fetchAllStatsAction() // Server + email + DNS + security stats -fetchConfigurationAction() // System configuration -fetchRecentLogsAction() // Log entries -fetchNetworkStatsAction() // Connection + throughput data - -// Email operations -fetchQueuedEmailsAction() // Pending emails -fetchSentEmailsAction() // Delivered emails -fetchFailedEmailsAction() // Failed emails -fetchSecurityIncidentsAction() // Security events -fetchBounceRecordsAction() // Bounce records -resendEmailAction(emailId) // Re-queue failed email -removeFromSuppressionAction(email) // Remove from suppression list - -// Certificates -fetchCertificateOverviewAction() // All certificates with summary -reprovisionCertificateAction(domain) // Reprovision a certificate -deleteCertificateAction(domain) // Delete a certificate -importCertificateAction(cert) // Import a certificate -fetchCertificateExport(domain) // Export (standalone function) - -// Remote Ingress -fetchRemoteIngressAction() // Edges + statuses -createRemoteIngressAction(data) // Create new edge -updateRemoteIngressAction(data) // Update edge settings -deleteRemoteIngressAction(id) // Remove edge -regenerateRemoteIngressSecretAction(id) // New secret -toggleRemoteIngressAction(id, enabled) // Enable/disable -clearNewEdgeSecretAction() // Dismiss secret banner -fetchConnectionToken(edgeId) // Get connection token (standalone function) - -// VPN -fetchVpnAction() // Clients + server status -createVpnClientAction(data) // Create new VPN client -deleteVpnClientAction(clientId) // Remove VPN client -toggleVpnClientAction(id, enabled) // Enable/disable -clearNewClientConfigAction() // Dismiss config banner -``` - -### Client-Side Routing - -``` -/overview β†’ Overview dashboard -/network β†’ Network monitoring -/emails β†’ Email management - /emails/queued β†’ Queued emails - /emails/sent β†’ Sent emails - /emails/failed β†’ Failed emails - /emails/security β†’ Security incidents -/certificates β†’ Certificate management -/remoteingress β†’ Remote ingress edge management -/vpn β†’ VPN client management -/routes β†’ Route & API token management -/logs β†’ Log viewer -/configuration β†’ System configuration -/security β†’ Security dashboard -``` - -URL state is synchronized with the UI β€” bookmarking and deep linking fully supported. - -## Development - -### Running Locally - -Start DcRouter with OpsServer enabled: - -```typescript -import { DcRouter } from '@serve.zone/dcrouter'; - -const router = new DcRouter({ - // OpsServer starts automatically on port 3000 - smartProxyConfig: { routes: [/* your routes */] } -}); - -await router.start(); -// Dashboard at http://localhost:3000 -``` - -### Building +The browser bundle is built from this package and served by the main dcrouter package. ```bash -# Build the bundle pnpm run bundle - -# Watch for development (auto-rebuild + restart) pnpm run watch ``` -The bundle is output to `./dist_serve/bundle.js` and served by the OpsServer. +The generated bundle is written into `dist_serve/` by the main build pipeline. -### Adding a New View +## When To Use This Package -1. Create a view component in `elements/`: -```typescript -import { DeesElement, customElement, html, css } from '@design.estate/dees-element'; - -@customElement('ops-view-myview') -export class OpsViewMyView extends DeesElement { - public static styles = [css`:host { display: block; padding: 24px; }`]; - - public render() { - return html`My View`; - } -} -``` - -2. Add it to the dashboard tabs in `ops-dashboard.ts` -3. Add the route in `router.ts` -4. Add any state management in `appstate.ts` +- Use it if you want the dashboard frontend as a package/module boundary. +- Use the main `@serve.zone/dcrouter` package if you want the server that actually serves this UI. ## License and Legal Information