Compare commits

...

10 Commits

Author SHA1 Message Date
6f4a5f19e7 v12.3.0
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 20:31:08 +00:00
9d8354e58f feat(docs,ops-dashboard): document unified database and reusable security profile and network target management 2026-04-02 20:31:08 +00:00
947637eed7 v12.2.6
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 18:49:52 +00:00
5202c2ea27 fix(ops-ui): improve operations table actions and modal form handling for profiles and network targets 2026-04-02 18:49:52 +00:00
6684dc43da v12.2.5
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 17:59:51 +00:00
04ec387ce5 fix(dcrouter): sync allowed tunnel edges when merged routes change 2026-04-02 17:59:51 +00:00
f145798f39 v12.2.4
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 17:27:05 +00:00
55f5465a9a fix(routes): support profile and target metadata in route creation and refresh remote ingress routes after config initialization 2026-04-02 17:27:05 +00:00
0577f45ced v12.2.3
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 16:27:35 +00:00
7d23617f15 fix(repo): no changes to commit 2026-04-02 16:27:35 +00:00
15 changed files with 328 additions and 140 deletions

View File

@@ -1,5 +1,40 @@
# Changelog
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
document unified database and reusable security profile and network target management
- Update project and interface documentation to replace separate storage/cache configuration with a unified database model
- Document new security profile and network target APIs, data models, and dashboard capabilities
- Add a global dashboard warning when the database is disabled so unavailable management features are clearly indicated
- Bump @design.estate/dees-catalog and @serve.zone/catalog to support the updated dashboard experience
## 2026-04-02 - 12.2.6 - fix(ops-ui)
improve operations table actions and modal form handling for profiles and network targets
- adds section headings for the Security Profiles and Network Targets views
- updates edit and delete actions to support in-row table actions in addition to context menus
- makes create and edit dialogs query forms safely from modal content and adds early returns when forms are unavailable
- enables the database configuration in the development watch server
## 2026-04-02 - 12.2.5 - fix(dcrouter)
sync allowed tunnel edges when merged routes change
- Triggers tunnelManager.syncAllowedEdges() after route updates are applied
- Keeps derived ports in the Rust hub binary aligned with merged route changes
## 2026-04-02 - 12.2.4 - fix(routes)
support profile and target metadata in route creation and refresh remote ingress routes after config initialization
- Re-applies routes to the remote ingress manager after config managers finish to avoid missing DB-backed routes during initialization
- Fetches profiles and targets when opening or authenticating into the routes view so route creation dropdowns are populated
- Includes selected security profile and network target metadata when creating programmatic routes and displays that metadata in route details
- Improves security profile forms by switching IP allow/block lists to list inputs instead of comma-separated text fields
- Updates UI dependencies including smartdb, dees-catalog, and serve.zone catalog
## 2026-04-02 - 12.2.3 - fix(repo)
no changes to commit
## 2026-04-02 - 12.2.2 - fix(route-config)
sync applied routes to remote ingress manager after route updates

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.2.2",
"version": "12.3.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -35,14 +35,14 @@
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.49.1",
"@design.estate/dees-catalog": "^3.50.2",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.3.1",
"@push.rocks/smartdata": "^7.1.3",
"@push.rocks/smartdb": "^2.0.0",
"@push.rocks/smartdb": "^2.1.1",
"@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartguard": "^3.1.0",
@@ -61,7 +61,7 @@
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartvpn": "1.19.1",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/catalog": "^2.10.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.15.3",
"@tsclass/tsclass": "^9.5.0",

48
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0
version: 7.1.0
'@design.estate/dees-catalog':
specifier: ^3.49.1
version: 3.49.1(@tiptap/pm@2.27.2)
specifier: ^3.50.2
version: 3.50.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -45,8 +45,8 @@ importers:
specifier: ^7.1.3
version: 7.1.3(socks@2.8.7)
'@push.rocks/smartdb':
specifier: ^2.0.0
version: 2.0.0
specifier: ^2.1.1
version: 2.1.1(@tiptap/pm@2.27.2)
'@push.rocks/smartdns':
specifier: ^7.9.0
version: 7.9.0
@@ -102,8 +102,8 @@ importers:
specifier: ^8.0.2
version: 8.0.2
'@serve.zone/catalog':
specifier: ^2.9.0
version: 2.9.0(@tiptap/pm@2.27.2)
specifier: ^2.10.0
version: 2.10.0(@tiptap/pm@2.27.2)
'@serve.zone/interfaces':
specifier: ^5.3.0
version: 5.3.0
@@ -350,8 +350,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.49.1':
resolution: {integrity: sha512-YyaRu6uep5wiqx2wnQeeWXstNRkkEfTAH7uA9XiWwM+TwbWH83esu5PR8L+J4akz3VsSW26JlfRI+7GoWTs2mw==}
'@design.estate/dees-catalog@3.50.2':
resolution: {integrity: sha512-oxB1kB3IxEwHgf+DjytTBilkDVVb8hryq465OhhzgBiJiHaNLPyBASAQaNTVp6eaORQGzyCmy/ac/GdQglZiIg==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -1141,8 +1141,8 @@ packages:
'@push.rocks/smartdata@7.1.3':
resolution: {integrity: sha512-7vQJ9pdRk450yn2m9tmGPdSRlQVmxFPZjHD4sGYsfqCQPg+GLFusu+H16zpf+jKzAq4F2ZBMPaYymJHXvXiVcw==}
'@push.rocks/smartdb@2.0.0':
resolution: {integrity: sha512-RGaXGOS+5c7Hru2XwoyavQuoZqrfIzUfF/AnnVA0GYOrj4P2S89fngp8QDczVyZq/IbkByYXz59foQmN/WDlWA==}
'@push.rocks/smartdb@2.1.1':
resolution: {integrity: sha512-bm+xYpuzSgS+EacNP3NppwNvpw9OZN3gmtVUgBdqyLLKYX0329bDN5X63V6vdrglFBV/+MKox43l8BQBwVdfjw==}
'@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
@@ -1583,8 +1583,8 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.9.0':
resolution: {integrity: sha512-7FgwS44pD/DFVj29jS0Kwwyn1i5h8cf4/yWMBEY8+8GO70ab3QctbcKMu+BVa1G3gIrpLqhpmxLFDoeL/zDnQA==}
'@serve.zone/catalog@2.10.0':
resolution: {integrity: sha512-/y3gDrf3UHXaDhLJtqJTeHSXOCKGQ4ou6Dd80tMxQYm8/I/OJmifkgerLKP05WdbMyj0pLp33QhjLElJrpME8Q==}
'@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
@@ -4339,7 +4339,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
'@cloudflare/workers-types': 4.20260317.1
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.0
'@push.rocks/smartdelay': 3.0.5
@@ -4868,7 +4868,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.49.1(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.50.2(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -4976,7 +4976,7 @@ snapshots:
'@design.estate/dees-wcctools@3.8.0':
dependencies:
'@design.estate/dees-domtools': 2.5.3
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.2
@@ -6127,9 +6127,19 @@ snapshots:
- supports-color
- vue
'@push.rocks/smartdb@2.0.0':
'@push.rocks/smartdb@2.1.1(@tiptap/pm@2.27.2)':
dependencies:
'@api.global/typedserver': 8.4.6(@tiptap/pm@2.27.2)
'@design.estate/dees-element': 2.2.4
'@push.rocks/smartrust': 1.3.2
transitivePeerDependencies:
- '@nuxt/kit'
- '@tiptap/pm'
- bufferutil
- react
- supports-color
- utf-8-validate
- vue
'@push.rocks/smartdelay@3.0.5':
dependencies:
@@ -6894,10 +6904,10 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@serve.zone/catalog@2.9.0(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.10.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.3
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.8.0
transitivePeerDependencies:

140
readme.md
View File

@@ -93,10 +93,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **Socket-handler mode** — direct socket passing eliminates internal port hops
- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput)
### 💾 Persistent Storage & Caching
- **Multiple storage backends**: filesystem, custom functions, or in-memory
- **Embedded cache database** via smartdata + smartdb (MongoDB-compatible)
### 💾 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
@@ -104,7 +105,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **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
- **Read-only configuration display** — DcRouter is configured through code
- **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
@@ -269,11 +272,8 @@ const router = new DcRouter({
],
},
// Persistent storage
storage: { fsPath: '/var/lib/dcrouter/data' },
// Cache database
cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
// Unified database (embedded LocalSmartDb or external MongoDB)
dbConfig: { enabled: true },
// TLS & ACME
tls: { contactEmail: 'admin@example.com' },
@@ -311,8 +311,7 @@ graph TB
CM[Certificate Manager<br/><i>smartacme v9</i>]
OS[OpsServer Dashboard]
MM[Metrics Manager]
SM[Storage Manager]
CD[Cache Database]
DB2[DcRouterDb<br/><i>smartdata + smartdb</i>]
end
subgraph "Backend Services"
@@ -339,8 +338,7 @@ graph TB
DC --> CM
DC --> OS
DC --> MM
DC --> SM
DC --> CD
DC --> DB2
SP --> WEB
SP --> API
@@ -365,8 +363,7 @@ graph TB
| **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) |
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
| **CacheDb** | `@push.rocks/smartdb` | Embedded MongoDB-compatible database (LocalSmartDb) for persistent caching |
| **DcRouterDb** | `@push.rocks/smartdata` + `@push.rocks/smartdb` | Unified database — embedded LocalSmartDb or external MongoDB for all persistence |
### How It Works
@@ -509,24 +506,16 @@ interface IDcRouterOptions {
};
dnsChallenge?: { cloudflareApiKey?: string };
// ── Storage & Caching ─────────────────────────────────────────
storage?: {
fsPath?: string;
readFunction?: (key: string) => Promise<string>;
writeFunction?: (key: string, value: string) => Promise<void>;
};
cacheConfig?: {
// ── 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
ttlConfig?: {
emails?: number; // default: 30 days
ipReputation?: number; // default: 1 day
bounces?: number; // default: 30 days
dkimKeys?: number; // default: 90 days
suppression?: number; // default: 30 days
};
seedOnEmpty?: boolean; // Seed default profiles/targets if DB is empty
seedData?: object; // Custom seed data
};
}
```
@@ -1213,49 +1202,55 @@ The OpsServer includes a **Certificates** view showing:
- One-click reprovisioning per domain
- Certificate import and export
## Storage & Caching
## Storage & Database
### StorageManager
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:
Provides a unified key-value interface with three backends:
### Embedded LocalSmartDb (Default)
Zero-config, file-based MongoDB-compatible database — no external services needed:
```typescript
// Filesystem backend
storage: { fsPath: '/var/lib/dcrouter/data' }
// Custom backend (Redis, S3, etc.)
storage: {
readFunction: async (key) => await redis.get(key),
writeFunction: async (key, value) => await redis.set(key, value)
}
// In-memory (development only — data lost on restart)
// Simply omit the storage config
dbConfig: { enabled: true }
// Data stored at ~/.serve.zone/dcrouter/tsmdb by default
```
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state, remote ingress edge registrations.
### External MongoDB
### Cache Database
An embedded MongoDB-compatible database (via smartdata + smartdb) for persistent caching with automatic TTL cleanup:
Connect to an existing MongoDB instance:
```typescript
cacheConfig: {
dbConfig: {
enabled: true,
storagePath: '~/.serve.zone/dcrouter/tsmdb',
mongoDbUrl: 'mongodb://localhost:27017',
dbName: 'dcrouter',
cleanupIntervalHours: 1,
ttlConfig: {
emails: 30, // days
ipReputation: 1, // days
bounces: 30, // days
dkimKeys: 90, // days
suppression: 30 // days
}
}
```
Cached document types: `CachedEmail`, `CachedIPReputation`.
### 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
@@ -1324,6 +1319,8 @@ The OpsServer provides a web-based management interface served on port 3000 by d
| 🔐 **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 |
@@ -1410,6 +1407,22 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
'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
@@ -1518,12 +1531,12 @@ const router = new DcRouter(options: IDcRouterOptions);
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager |
| `storageManager` | `StorageManager` | Storage backend |
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
| `metricsManager` | `MetricsManager` | Metrics collector |
| `cacheDb` | `CacheDb` | Cache database instance |
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
| `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
@@ -1589,7 +1602,8 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
| `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.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 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

View File

@@ -49,8 +49,7 @@ const devRouter = new DcRouter({
{ clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' },
],
},
// Disable db/mongo for dev
dbConfig: { enabled: false },
dbConfig: { enabled: true },
});
console.log('Starting DcRouter in development mode...');

View File

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

View File

@@ -478,11 +478,15 @@ export class DcRouter {
}
: undefined,
this.referenceResolver,
// Sync merged routes to RemoteIngressManager whenever routes change
// Sync merged routes to RemoteIngressManager whenever routes change,
// then push updated derived ports to the Rust hub binary
(routes) => {
if (this.remoteIngressManager) {
this.remoteIngressManager.setRoutes(routes as any[]);
}
if (this.tunnelManager) {
this.tunnelManager.syncAllowedEdges();
}
},
);
this.apiTokenManager = new ApiTokenManager();
@@ -2060,6 +2064,13 @@ export class DcRouter {
const currentRoutes = this.constructorRoutes;
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
// Race-condition fix: if ConfigManagers finished before us, re-apply routes
// so the callback delivers the full merged set (including DB-stored routes)
// to our newly-created remoteIngressManager.
if (this.routeConfigManager) {
await this.routeConfigManager.applyRoutes();
}
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
const riCfg = this.options.remoteIngressConfig;
let tlsConfig: { certPem: string; keyPem: string } | undefined;

View File

@@ -90,6 +90,13 @@ interface IIdentity {
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
#### 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 |
#### Remote Ingress Interfaces
| Interface | Description |
|-----------|-------------|
@@ -241,6 +248,26 @@ interface ICertificateInfo {
| `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.

View File

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

View File

@@ -427,6 +427,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
if (viewName === 'routes' && currentState.activeView !== 'routes') {
setTimeout(() => {
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
// Also fetch profiles/targets for the Create Route dropdowns
profilesTargetsStatePart.dispatchAction(fetchProfilesAndTargetsAction, null);
}, 100);
}
@@ -1413,6 +1415,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{
route: any;
enabled?: boolean;
metadata?: any;
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
@@ -1426,6 +1429,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
identity: context.identity!,
route: dataArg.route,
enabled: dataArg.enabled,
metadata: dataArg.metadata,
});
return await actionContext!.dispatch(fetchMergedRoutesAction, null);

View File

@@ -2,7 +2,6 @@ import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { appRouter } from '../router.js';
import {
DeesElement,
css,
@@ -43,6 +42,12 @@ export class OpsDashboard extends DeesElement {
theme: 'light',
};
@state() accessor configState: appstate.IConfigState = {
config: null,
isLoading: false,
error: null,
};
// Store viewTabs as a property to maintain object references
private viewTabs = [
{
@@ -112,6 +117,20 @@ export class OpsDashboard extends DeesElement {
},
];
private get globalMessages() {
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
const config = this.configState.config;
if (config && !config.cache.enabled) {
messages.push({
id: 'db-disabled',
type: 'warning',
message: 'Database is disabled. Creating and editing routes, profiles, targets, and API tokens is not available.',
dismissible: false,
});
}
return messages;
}
/**
* Get the current view tab based on the UI state's activeView.
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
@@ -137,6 +156,14 @@ export class OpsDashboard extends DeesElement {
});
this.rxSubscriptions.push(loginSubscription);
// Subscribe to config state (for global warnings)
const configSubscription = appstate.configStatePart
.select((stateArg) => stateArg)
.subscribe((configState) => {
this.configState = configState;
});
this.rxSubscriptions.push(configSubscription);
// Subscribe to UI state
const uiSubscription = appstate.uiStatePart
.select((stateArg) => stateArg)
@@ -205,6 +232,7 @@ export class OpsDashboard extends DeesElement {
name="DCRouter OpsServer"
.viewTabs=${this.viewTabs}
.selectedView=${this.currentViewTab}
.globalMessages=${this.globalMessages}
>
</dees-simple-appdash>
</dees-simple-login>

View File

@@ -64,6 +64,7 @@ export class OpsViewNetworkTargets extends DeesElement {
];
return html`
<ops-sectionheading>Network Targets</ops-sectionheading>
<div class="targetsContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
@@ -81,8 +82,8 @@ export class OpsViewNetworkTargets extends DeesElement {
name: 'Create Target',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async (_: any, table: any) => {
await this.showCreateTargetDialog(table);
actionFunc: async () => {
await this.showCreateTargetDialog();
},
},
{
@@ -96,16 +97,18 @@ export class OpsViewNetworkTargets extends DeesElement {
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
actionFunc: async (target: interfaces.data.INetworkTarget, table: any) => {
await this.showEditTargetDialog(target, table);
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const target = actionData.item as interfaces.data.INetworkTarget;
await this.showEditTargetDialog(target);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['contextmenu' as const],
actionFunc: async (target: interfaces.data.INetworkTarget) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const target = actionData.item as interfaces.data.INetworkTarget;
await this.deleteTarget(target);
},
},
@@ -115,7 +118,7 @@ export class OpsViewNetworkTargets extends DeesElement {
`;
}
private async showCreateTargetDialog(table: any) {
private async showCreateTargetDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Create Network Target',
@@ -128,10 +131,12 @@ export class OpsViewNetworkTargets extends DeesElement {
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Create',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createTargetAction, {
@@ -143,12 +148,11 @@ export class OpsViewNetworkTargets extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}
private async showEditTargetDialog(target: interfaces.data.INetworkTarget, table: any) {
private async showEditTargetDialog(target: interfaces.data.INetworkTarget) {
const hostStr = Array.isArray(target.host) ? target.host.join(', ') : target.host;
const { DeesModal } = await import('@design.estate/dees-catalog');
@@ -163,10 +167,12 @@ export class OpsViewNetworkTargets extends DeesElement {
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Save',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateTargetAction, {
@@ -179,7 +185,6 @@ export class OpsViewNetworkTargets extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}

View File

@@ -24,6 +24,14 @@ export class OpsViewRoutes extends DeesElement {
lastUpdated: 0,
};
@state() accessor profilesTargetsState: appstate.IProfilesTargetsState = {
profiles: [],
targets: [],
isLoading: false,
error: null,
lastUpdated: 0,
};
constructor() {
super();
const sub = appstate.routeManagementStatePart
@@ -33,6 +41,13 @@ export class OpsViewRoutes extends DeesElement {
});
this.rxSubscriptions.push(sub);
const ptSub = appstate.profilesTargetsStatePart
.select((s) => s)
.subscribe((ptState) => {
this.profilesTargetsState = ptState;
});
this.rxSubscriptions.push(ptSub);
// Re-fetch routes when user logs in (fixes race condition where
// the view is created before authentication completes)
const loginSub = appstate.loginStatePart
@@ -40,6 +55,7 @@ export class OpsViewRoutes extends DeesElement {
.subscribe((isLoggedIn) => {
if (isLoggedIn) {
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
}
});
this.rxSubscriptions.push(loginSub);
@@ -145,6 +161,7 @@ export class OpsViewRoutes extends DeesElement {
enabled: mr.enabled,
tags,
id: mr.storedRouteId || mr.route.name || undefined,
metadata: mr.metadata,
};
});
@@ -275,6 +292,7 @@ export class OpsViewRoutes extends DeesElement {
});
} else {
// Programmatic route
const meta = merged.metadata;
await DeesModal.createAndShow({
heading: `Route: ${merged.route.name}`,
content: html`
@@ -282,6 +300,8 @@ export class OpsViewRoutes extends DeesElement {
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
${meta?.securityProfileName ? html`<p>Security Profile: <strong style="color: #a78bfa;">${meta.securityProfileName}</strong></p>` : ''}
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
</div>
`,
menuOptions: [
@@ -319,6 +339,24 @@ export class OpsViewRoutes extends DeesElement {
private async showCreateRouteDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;
const targets = this.profilesTargetsState.targets;
// Build dropdown options for profiles and targets
const profileOptions = [
{ key: '', option: '(none — inline security)' },
...profiles.map((p) => ({
key: p.id,
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
})),
];
const targetOptions = [
{ key: '', option: '(none — inline target)' },
...targets.map((t) => ({
key: t.id,
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
})),
];
await DeesModal.createAndShow({
heading: 'Add Programmatic Route',
@@ -327,8 +365,10 @@ export class OpsViewRoutes extends DeesElement {
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'}></dees-input-text>
</dees-form>
`,
menuOptions: [
@@ -362,15 +402,27 @@ export class OpsViewRoutes extends DeesElement {
targets: [
{
host: formData.targetHost || 'localhost',
port: parseInt(formData.targetPort, 10),
port: parseInt(formData.targetPort, 10) || 443,
},
],
},
};
// Build metadata if profile/target selected
const metadata: any = {};
if (formData.securityProfileRef) {
metadata.securityProfileRef = formData.securityProfileRef;
}
if (formData.networkTargetRef) {
metadata.networkTargetRef = formData.networkTargetRef;
}
await appstate.routeManagementStatePart.dispatchAction(
appstate.createRouteAction,
{ route },
{
route,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
},
);
await modalArg.destroy();
},

View File

@@ -64,6 +64,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
];
return html`
<ops-sectionheading>Security Profiles</ops-sectionheading>
<div class="profilesContainer">
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
<dees-table
@@ -89,8 +90,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
name: 'Create Profile',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async (_: any, table: any) => {
await this.showCreateProfileDialog(table);
actionFunc: async () => {
await this.showCreateProfileDialog();
},
},
{
@@ -104,16 +105,18 @@ export class OpsViewSecurityProfiles extends DeesElement {
{
name: 'Edit',
iconName: 'lucide:pencil',
type: ['contextmenu' as const],
actionFunc: async (profile: interfaces.data.ISecurityProfile, table: any) => {
await this.showEditProfileDialog(profile, table);
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const profile = actionData.item as interfaces.data.ISecurityProfile;
await this.showEditProfileDialog(profile);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['contextmenu' as const],
actionFunc: async (profile: interfaces.data.ISecurityProfile) => {
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const profile = actionData.item as interfaces.data.ISecurityProfile;
await this.deleteProfile(profile);
},
},
@@ -123,7 +126,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
`;
}
private async showCreateProfileDialog(table: any) {
private async showCreateProfileDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Create Security Profile',
@@ -131,43 +134,40 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Create',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
name: String(data.name),
description: data.description ? String(data.description) : undefined,
security: {
...(ipAllowList ? { ipAllowList } : {}),
...(ipBlockList ? { ipBlockList } : {}),
...(ipAllowList.length > 0 ? { ipAllowList } : {}),
...(ipBlockList.length > 0 ? { ipBlockList } : {}),
...(maxConnections ? { maxConnections } : {}),
},
});
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile, table: any) {
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile) {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: `Edit Profile: ${profile.name}`,
@@ -175,23 +175,21 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'} .value=${(profile.security?.ipAllowList || []).join(', ')}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'} .value=${(profile.security?.ipBlockList || []).join(', ')}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipAllowList || []}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipBlockList || []}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Save',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
@@ -207,7 +205,6 @@ export class OpsViewSecurityProfiles extends DeesElement {
modalArg.destroy();
},
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
}

View File

@@ -68,6 +68,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- API token creation, revocation, and scope management
- Routes tab and API Tokens tab in unified view
### 🛡️ 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
### ⚙️ Configuration
- Read-only display of current system configuration
- Status badges for boolean values (enabled/disabled)