feat(routes,email): persist system DNS routes with runtime hydration and add reusable email ops DNS helpers
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user