Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd6130013c | |||
| 0f814bbcdd | |||
| 8ec94b7dae | |||
| d5dfe439c7 | |||
| aaf3c9cb1c | |||
| abde872ab2 | |||
| ca2d2b09ad | |||
| fb7d4d988b | |||
| 26e6eea5d5 | |||
| 2458dd08d8 | |||
| dee648b3bc | |||
| f4ed32cee4 |
44
changelog.md
44
changelog.md
@@ -1,5 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-08 - 11.2.1 - fix(deps)
|
||||
bump devDependency @git.zone/tstest to ^3.3.0 and dependency @push.rocks/smartproxy to ^25.9.2
|
||||
|
||||
- Bumped devDependency @git.zone/tstest: ^3.2.0 -> ^3.3.0
|
||||
- Bumped dependency @push.rocks/smartproxy: ^25.9.1 -> ^25.9.2
|
||||
- Current package version: 11.2.0 — recommended patch release to 11.2.1
|
||||
|
||||
## 2026-03-06 - 11.2.0 - feat(apiclient)
|
||||
add typed, object-oriented API client documentation and interfaces; document builders, resource managers, and new programmatic endpoints
|
||||
|
||||
- Add new @serve.zone/dcrouter-apiclient documentation (ts_apiclient/readme.md) and publish ordering (ts_apiclient/tspublish.json).
|
||||
- Document OO resource classes, fluent builders, auth modes, examples, and API surface for routes, certificates, apiTokens, remoteIngress, stats, config, logs, emails, and radius.
|
||||
- Update main readme: add API Client section, list new client methods, add package entry for @serve.zone/dcrouter-apiclient, and add apiclient test coverage entry.
|
||||
- Update interfaces readme: add Route Management and API Token Management request interfaces and email method changes (getAllEmails, getEmailDetail).
|
||||
- API reference changes: consolidate email endpoints (getAllEmails/getEmailDetail), add route and api token management methods, rename getLogs to getRecentLogs and add getLogStream.
|
||||
- Update web docs to include route & API token management pages and ops view (ops-view-routes)
|
||||
|
||||
## 2026-03-06 - 11.1.0 - feat(apiclient)
|
||||
add TypeScript API client (ts_apiclient) with resource managers and package exports
|
||||
|
||||
- Add new ts_apiclient module providing DcRouterApiClient and resource managers: routes, certificates, api tokens, remote ingress, emails, stats, config, logs, and radius (with sub-managers).
|
||||
- Add resource classes and builders (Route, RemoteIngress, ApiToken, Certificate, Email) and convenience manager APIs for common operations.
|
||||
- Export apiclient in package.json (exports and files) and add ts_apiclient index and plugins wrapper for @api.global/typedrequest.
|
||||
- Add comprehensive tests for the API client (test/test.apiclient.ts).
|
||||
- Bump devDependencies: @git.zone/tsbuild -> ^4.3.0 and @types/node -> ^25.3.5
|
||||
|
||||
## 2026-03-05 - 11.0.51 - fix(build)
|
||||
include HTML files in tsbundle output and bump tsbuild/tsbundle devDependencies
|
||||
|
||||
- Add includeFiles: ["./html/**/*.html"] to bundler config in npmextra.json so HTML assets are included in the bundle
|
||||
- Bump devDependencies: @git.zone/tsbuild ^4.2.4 -> ^4.2.6, @git.zone/tsbundle ^2.9.0 -> ^2.9.1 (non-breaking tooling updates)
|
||||
|
||||
## 2026-03-05 - 11.0.50 - fix(devDependencies)
|
||||
bump @git.zone/tsbuild to ^4.2.4
|
||||
|
||||
- updated devDependency @git.zone/tsbuild from ^4.2.3 to ^4.2.4
|
||||
- no other package changes
|
||||
|
||||
## 2026-03-05 - 11.0.49 - fix(dcrouter)
|
||||
no changes detected
|
||||
|
||||
- No files changed in this commit
|
||||
- Working tree unchanged; no version bump required
|
||||
|
||||
## 2026-03-05 - 11.0.48 - fix(deps)
|
||||
bump @git.zone/tsbuild to ^4.2.3
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"to": "./dist_serve/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"bundler": "esbuild",
|
||||
"production": true
|
||||
"production": true,
|
||||
"includeFiles": ["./html/**/*.html"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
17
package.json
17
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "11.0.48",
|
||||
"version": "11.2.1",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./interfaces": "./dist_ts_interfaces/index.js"
|
||||
"./interfaces": "./dist_ts_interfaces/index.js",
|
||||
"./apiclient": "./dist_ts_apiclient/index.js"
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
@@ -19,12 +20,12 @@
|
||||
"watch": "tswatch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.2.3",
|
||||
"@git.zone/tsbundle": "^2.9.0",
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tsbundle": "^2.9.1",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.2.0",
|
||||
"@git.zone/tstest": "^3.3.0",
|
||||
"@git.zone/tswatch": "^3.2.5",
|
||||
"@types/node": "^25.3.3"
|
||||
"@types/node": "^25.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.3.0",
|
||||
@@ -50,7 +51,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^25.9.1",
|
||||
"@push.rocks/smartproxy": "^25.9.2",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
@@ -100,10 +101,12 @@
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"ts_apiclient/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"dist_ts_apiclient/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
|
||||
515
pnpm-lock.yaml
generated
515
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
110
readme.md
110
readme.md
@@ -26,6 +26,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [Storage & Caching](#storage--caching)
|
||||
- [Security Features](#security-features)
|
||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||
- [API Client](#api-client)
|
||||
- [API Reference](#api-reference)
|
||||
- [Sub-Modules](#sub-modules)
|
||||
- [Testing](#testing)
|
||||
@@ -90,6 +91,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Remote ingress management** with connection token generation and one-click copy
|
||||
- **Read-only configuration display** — DcRouter is configured through code
|
||||
|
||||
### 🔧 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
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -1038,12 +1046,9 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'getCombinedMetrics' // All metrics in one call
|
||||
|
||||
// Email Operations
|
||||
'getQueuedEmails' // Emails pending delivery
|
||||
'getSentEmails' // Successfully delivered emails
|
||||
'getFailedEmails' // Failed emails
|
||||
'getAllEmails' // List all emails (queued/sent/failed)
|
||||
'getEmailDetail' // Full detail for a specific email
|
||||
'resendEmail' // Re-queue a failed email
|
||||
'getBounceRecords' // Bounce records
|
||||
'removeFromSuppressionList' // Unsuppress an address
|
||||
|
||||
// Certificates
|
||||
'getCertificateOverview' // Domain-centric certificate status
|
||||
@@ -1062,11 +1067,28 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'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
|
||||
'getLogs' // Retrieve system logs
|
||||
'getRecentLogs' // Retrieve system logs with filtering
|
||||
'getLogStream' // Stream live logs
|
||||
|
||||
// RADIUS
|
||||
'getRadiusSessions' // Active RADIUS sessions
|
||||
@@ -1080,6 +1102,77 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'testVlanAssignment' // Test what VLAN a MAC gets
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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' });
|
||||
await client.login('admin', 'password');
|
||||
|
||||
// OO resource instances with methods
|
||||
const { routes } = await client.routes.list();
|
||||
await routes[0].toggle(false);
|
||||
|
||||
// Builder pattern for creation
|
||||
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' })
|
||||
.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
|
||||
|
||||
| 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()` |
|
||||
|
||||
See the [full API client documentation](./ts_apiclient/readme.md) for detailed usage of every manager, builder, and resource class.
|
||||
|
||||
## API Reference
|
||||
|
||||
### DcRouter Class
|
||||
@@ -1144,12 +1237,14 @@ DcRouter is published as a monorepo with separately-installable interface and we
|
||||
|---------|-------------|---------|
|
||||
| [`@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 interfaces directly from the main package:
|
||||
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
|
||||
@@ -1171,6 +1266,7 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
|
||||
| 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 |
|
||||
|
||||
376
test/test.apiclient.ts
Normal file
376
test/test.apiclient.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
DcRouterApiClient,
|
||||
Route,
|
||||
RouteBuilder,
|
||||
RouteManager,
|
||||
Certificate,
|
||||
CertificateManager,
|
||||
ApiToken,
|
||||
ApiTokenBuilder,
|
||||
ApiTokenManager,
|
||||
RemoteIngress,
|
||||
RemoteIngressBuilder,
|
||||
RemoteIngressManager,
|
||||
Email,
|
||||
EmailManager,
|
||||
StatsManager,
|
||||
ConfigManager,
|
||||
LogManager,
|
||||
RadiusManager,
|
||||
RadiusClientManager,
|
||||
RadiusVlanManager,
|
||||
RadiusSessionManager,
|
||||
} from '../ts_apiclient/index.js';
|
||||
|
||||
// =============================================================================
|
||||
// Instantiation & Structure
|
||||
// =============================================================================
|
||||
|
||||
tap.test('DcRouterApiClient - should instantiate with baseUrl', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client).toBeTruthy();
|
||||
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||
expect(client.identity).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should strip trailing slashes from baseUrl', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000///' });
|
||||
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should accept optional apiToken', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_test_token',
|
||||
});
|
||||
expect(client.apiToken).toEqual('dcr_test_token');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - should have all resource managers', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client.routes).toBeInstanceOf(RouteManager);
|
||||
expect(client.certificates).toBeInstanceOf(CertificateManager);
|
||||
expect(client.apiTokens).toBeInstanceOf(ApiTokenManager);
|
||||
expect(client.remoteIngress).toBeInstanceOf(RemoteIngressManager);
|
||||
expect(client.stats).toBeInstanceOf(StatsManager);
|
||||
expect(client.config).toBeInstanceOf(ConfigManager);
|
||||
expect(client.logs).toBeInstanceOf(LogManager);
|
||||
expect(client.emails).toBeInstanceOf(EmailManager);
|
||||
expect(client.radius).toBeInstanceOf(RadiusManager);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// buildRequestPayload
|
||||
// =============================================================================
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload includes identity when set', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const identity = {
|
||||
jwt: 'test-jwt',
|
||||
userId: 'user1',
|
||||
name: 'Admin',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
client.identity = identity;
|
||||
|
||||
const payload = client.buildRequestPayload({ extra: 'data' });
|
||||
expect(payload.identity).toEqual(identity);
|
||||
expect(payload.extra).toEqual('data');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload includes apiToken when set', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_abc123',
|
||||
});
|
||||
|
||||
const payload = client.buildRequestPayload();
|
||||
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||
});
|
||||
|
||||
tap.test('DcRouterApiClient - buildRequestPayload with both identity and apiToken', async () => {
|
||||
const client = new DcRouterApiClient({
|
||||
baseUrl: 'https://localhost:3000',
|
||||
apiToken: 'dcr_abc123',
|
||||
});
|
||||
client.identity = {
|
||||
jwt: 'test-jwt',
|
||||
userId: 'user1',
|
||||
name: 'Admin',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
|
||||
const payload = client.buildRequestPayload({ foo: 'bar' });
|
||||
expect(payload.identity).toBeTruthy();
|
||||
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||
expect(payload.foo).toEqual('bar');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Route Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RouteBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.routes.build();
|
||||
expect(builder).toBeInstanceOf(RouteBuilder);
|
||||
|
||||
// Fluent methods return `this` (same reference)
|
||||
const result = builder
|
||||
.setName('test-route')
|
||||
.setMatch({ ports: 443, domains: 'example.com' })
|
||||
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||
.setEnabled(true);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// ApiToken Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('ApiTokenBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.apiTokens.build();
|
||||
expect(builder).toBeInstanceOf(ApiTokenBuilder);
|
||||
|
||||
const result = builder
|
||||
.setName('ci-token')
|
||||
.setScopes(['routes:read', 'routes:write'])
|
||||
.addScope('config:read')
|
||||
.setExpiresInDays(30);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RemoteIngress Builder
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RemoteIngressBuilder - should support fluent builder pattern', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const builder = client.remoteIngress.build();
|
||||
expect(builder).toBeInstanceOf(RemoteIngressBuilder);
|
||||
|
||||
const result = builder
|
||||
.setName('edge-1')
|
||||
.setListenPorts([80, 443])
|
||||
.setAutoDerivePorts(true)
|
||||
.setTags(['production']);
|
||||
|
||||
expect(result === builder).toBeTrue();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Route resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Route - should hydrate from IMergedRoute data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const route = new Route(client, {
|
||||
route: {
|
||||
name: 'test-route',
|
||||
match: { ports: 443, domains: 'example.com' },
|
||||
action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }] },
|
||||
},
|
||||
source: 'programmatic',
|
||||
enabled: true,
|
||||
overridden: false,
|
||||
storedRouteId: 'route-123',
|
||||
createdAt: 1000,
|
||||
updatedAt: 2000,
|
||||
});
|
||||
|
||||
expect(route.name).toEqual('test-route');
|
||||
expect(route.source).toEqual('programmatic');
|
||||
expect(route.enabled).toEqual(true);
|
||||
expect(route.overridden).toEqual(false);
|
||||
expect(route.storedRouteId).toEqual('route-123');
|
||||
expect(route.routeConfig.match.ports).toEqual(443);
|
||||
});
|
||||
|
||||
tap.test('Route - should throw on update/delete/toggle for hardcoded routes', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const route = new Route(client, {
|
||||
route: {
|
||||
name: 'hardcoded-route',
|
||||
match: { ports: 80 },
|
||||
action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
|
||||
},
|
||||
source: 'hardcoded',
|
||||
enabled: true,
|
||||
overridden: false,
|
||||
// No storedRouteId for hardcoded routes
|
||||
});
|
||||
|
||||
let updateError: Error | undefined;
|
||||
try {
|
||||
await route.update({ name: 'new-name' });
|
||||
} catch (e) {
|
||||
updateError = e as Error;
|
||||
}
|
||||
expect(updateError).toBeTruthy();
|
||||
expect(updateError!.message).toInclude('hardcoded');
|
||||
|
||||
let deleteError: Error | undefined;
|
||||
try {
|
||||
await route.delete();
|
||||
} catch (e) {
|
||||
deleteError = e as Error;
|
||||
}
|
||||
expect(deleteError).toBeTruthy();
|
||||
|
||||
let toggleError: Error | undefined;
|
||||
try {
|
||||
await route.toggle(false);
|
||||
} catch (e) {
|
||||
toggleError = e as Error;
|
||||
}
|
||||
expect(toggleError).toBeTruthy();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Certificate resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Certificate - should hydrate from ICertificateInfo data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const cert = new Certificate(client, {
|
||||
domain: 'example.com',
|
||||
routeNames: ['main-route'],
|
||||
status: 'valid',
|
||||
source: 'acme',
|
||||
tlsMode: 'terminate',
|
||||
expiryDate: '2027-01-01T00:00:00Z',
|
||||
issuer: "Let's Encrypt",
|
||||
canReprovision: true,
|
||||
});
|
||||
|
||||
expect(cert.domain).toEqual('example.com');
|
||||
expect(cert.status).toEqual('valid');
|
||||
expect(cert.source).toEqual('acme');
|
||||
expect(cert.canReprovision).toEqual(true);
|
||||
expect(cert.routeNames.length).toEqual(1);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// ApiToken resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('ApiToken - should hydrate from IApiTokenInfo data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const token = new ApiToken(
|
||||
client,
|
||||
{
|
||||
id: 'token-1',
|
||||
name: 'ci-token',
|
||||
scopes: ['routes:read', 'routes:write'],
|
||||
createdAt: Date.now(),
|
||||
expiresAt: null,
|
||||
lastUsedAt: null,
|
||||
enabled: true,
|
||||
},
|
||||
'dcr_secret_value',
|
||||
);
|
||||
|
||||
expect(token.id).toEqual('token-1');
|
||||
expect(token.name).toEqual('ci-token');
|
||||
expect(token.scopes.length).toEqual(2);
|
||||
expect(token.enabled).toEqual(true);
|
||||
expect(token.tokenValue).toEqual('dcr_secret_value');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RemoteIngress resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RemoteIngress - should hydrate from IRemoteIngress data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const edge = new RemoteIngress(client, {
|
||||
id: 'edge-1',
|
||||
name: 'test-edge',
|
||||
secret: 'secret123',
|
||||
listenPorts: [80, 443],
|
||||
enabled: true,
|
||||
autoDerivePorts: true,
|
||||
tags: ['prod'],
|
||||
createdAt: 1000,
|
||||
updatedAt: 2000,
|
||||
effectiveListenPorts: [80, 443, 8080],
|
||||
manualPorts: [80, 443],
|
||||
derivedPorts: [8080],
|
||||
});
|
||||
|
||||
expect(edge.id).toEqual('edge-1');
|
||||
expect(edge.name).toEqual('test-edge');
|
||||
expect(edge.listenPorts.length).toEqual(2);
|
||||
expect(edge.effectiveListenPorts!.length).toEqual(3);
|
||||
expect(edge.autoDerivePorts).toEqual(true);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Email resource class
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Email - should hydrate from IEmail data', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
const email = new Email(client, {
|
||||
id: 'email-1',
|
||||
direction: 'inbound',
|
||||
status: 'delivered',
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Test email',
|
||||
timestamp: '2026-03-06T00:00:00Z',
|
||||
messageId: '<msg-1@example.com>',
|
||||
size: '1234',
|
||||
});
|
||||
|
||||
expect(email.id).toEqual('email-1');
|
||||
expect(email.direction).toEqual('inbound');
|
||||
expect(email.status).toEqual('delivered');
|
||||
expect(email.from).toEqual('sender@example.com');
|
||||
expect(email.subject).toEqual('Test email');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// RadiusManager structure
|
||||
// =============================================================================
|
||||
|
||||
tap.test('RadiusManager - should have sub-managers', async () => {
|
||||
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||
expect(client.radius.clients).toBeInstanceOf(RadiusClientManager);
|
||||
expect(client.radius.vlans).toBeInstanceOf(RadiusVlanManager);
|
||||
expect(client.radius.sessions).toBeInstanceOf(RadiusSessionManager);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Exports verification
|
||||
// =============================================================================
|
||||
|
||||
tap.test('Exports - all expected classes should be importable', async () => {
|
||||
expect(DcRouterApiClient).toBeTruthy();
|
||||
expect(Route).toBeTruthy();
|
||||
expect(RouteBuilder).toBeTruthy();
|
||||
expect(RouteManager).toBeTruthy();
|
||||
expect(Certificate).toBeTruthy();
|
||||
expect(CertificateManager).toBeTruthy();
|
||||
expect(ApiToken).toBeTruthy();
|
||||
expect(ApiTokenBuilder).toBeTruthy();
|
||||
expect(ApiTokenManager).toBeTruthy();
|
||||
expect(RemoteIngress).toBeTruthy();
|
||||
expect(RemoteIngressBuilder).toBeTruthy();
|
||||
expect(RemoteIngressManager).toBeTruthy();
|
||||
expect(Email).toBeTruthy();
|
||||
expect(EmailManager).toBeTruthy();
|
||||
expect(StatsManager).toBeTruthy();
|
||||
expect(ConfigManager).toBeTruthy();
|
||||
expect(LogManager).toBeTruthy();
|
||||
expect(RadiusManager).toBeTruthy();
|
||||
expect(RadiusClientManager).toBeTruthy();
|
||||
expect(RadiusVlanManager).toBeTruthy();
|
||||
expect(RadiusSessionManager).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.0.48',
|
||||
version: '11.2.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
157
ts_apiclient/classes.apitoken.ts
Normal file
157
ts_apiclient/classes.apitoken.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class ApiToken {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IApiTokenInfo
|
||||
public id: string;
|
||||
public name: string;
|
||||
public scopes: interfaces.data.TApiTokenScope[];
|
||||
public createdAt: number;
|
||||
public expiresAt: number | null;
|
||||
public lastUsedAt: number | null;
|
||||
public enabled: boolean;
|
||||
|
||||
/** Only set on creation or roll. Not persisted on server side. */
|
||||
public tokenValue?: string;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IApiTokenInfo, tokenValue?: string) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.scopes = data.scopes;
|
||||
this.createdAt = data.createdAt;
|
||||
this.expiresAt = data.expiresAt;
|
||||
this.lastUsedAt = data.lastUsedAt;
|
||||
this.enabled = data.enabled;
|
||||
this.tokenValue = tokenValue;
|
||||
}
|
||||
|
||||
public async revoke(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RevokeApiToken>(
|
||||
'revokeApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to revoke token');
|
||||
}
|
||||
}
|
||||
|
||||
public async roll(): Promise<string> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RollApiToken>(
|
||||
'rollApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to roll token');
|
||||
}
|
||||
this.tokenValue = response.tokenValue;
|
||||
return response.tokenValue!;
|
||||
}
|
||||
|
||||
public async toggle(enabled: boolean): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleApiToken>(
|
||||
'toggleApiToken',
|
||||
this.clientRef.buildRequestPayload({ id: this.id, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to toggle token');
|
||||
}
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiTokenBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private tokenName: string = '';
|
||||
private tokenScopes: interfaces.data.TApiTokenScope[] = [];
|
||||
private tokenExpiresInDays?: number | null;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.tokenName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setScopes(scopes: interfaces.data.TApiTokenScope[]): this {
|
||||
this.tokenScopes = scopes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public addScope(scope: interfaces.data.TApiTokenScope): this {
|
||||
if (!this.tokenScopes.includes(scope)) {
|
||||
this.tokenScopes.push(scope);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public setExpiresInDays(days: number | null): this {
|
||||
this.tokenExpiresInDays = days;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<ApiToken> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateApiToken>(
|
||||
'createApiToken',
|
||||
this.clientRef.buildRequestPayload({
|
||||
name: this.tokenName,
|
||||
scopes: this.tokenScopes,
|
||||
expiresInDays: this.tokenExpiresInDays,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create API token');
|
||||
}
|
||||
return new ApiToken(
|
||||
this.clientRef,
|
||||
{
|
||||
id: response.tokenId!,
|
||||
name: this.tokenName,
|
||||
scopes: this.tokenScopes,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: this.tokenExpiresInDays
|
||||
? Date.now() + this.tokenExpiresInDays * 24 * 60 * 60 * 1000
|
||||
: null,
|
||||
lastUsedAt: null,
|
||||
enabled: true,
|
||||
},
|
||||
response.tokenValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiTokenManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<ApiToken[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ListApiTokens>(
|
||||
'listApiTokens',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.tokens.map((t) => new ApiToken(this.clientRef, t));
|
||||
}
|
||||
|
||||
public async create(options: {
|
||||
name: string;
|
||||
scopes: interfaces.data.TApiTokenScope[];
|
||||
expiresInDays?: number | null;
|
||||
}): Promise<ApiToken> {
|
||||
return this.build()
|
||||
.setName(options.name)
|
||||
.setScopes(options.scopes)
|
||||
.setExpiresInDays(options.expiresInDays ?? null)
|
||||
.save();
|
||||
}
|
||||
|
||||
public build(): ApiTokenBuilder {
|
||||
return new ApiTokenBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
123
ts_apiclient/classes.certificate.ts
Normal file
123
ts_apiclient/classes.certificate.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Certificate {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from ICertificateInfo
|
||||
public domain: string;
|
||||
public routeNames: string[];
|
||||
public status: interfaces.requests.TCertificateStatus;
|
||||
public source: interfaces.requests.TCertificateSource;
|
||||
public tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||
public expiryDate?: string;
|
||||
public issuer?: string;
|
||||
public issuedAt?: string;
|
||||
public error?: string;
|
||||
public canReprovision: boolean;
|
||||
public backoffInfo?: {
|
||||
failures: number;
|
||||
retryAfter?: string;
|
||||
lastError?: string;
|
||||
};
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.ICertificateInfo) {
|
||||
this.clientRef = clientRef;
|
||||
this.domain = data.domain;
|
||||
this.routeNames = data.routeNames;
|
||||
this.status = data.status;
|
||||
this.source = data.source;
|
||||
this.tlsMode = data.tlsMode;
|
||||
this.expiryDate = data.expiryDate;
|
||||
this.issuer = data.issuer;
|
||||
this.issuedAt = data.issuedAt;
|
||||
this.error = data.error;
|
||||
this.canReprovision = data.canReprovision;
|
||||
this.backoffInfo = data.backoffInfo;
|
||||
}
|
||||
|
||||
public async reprovision(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||
'reprovisionCertificateDomain',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to reprovision certificate');
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteCertificate>(
|
||||
'deleteCertificate',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete certificate');
|
||||
}
|
||||
}
|
||||
|
||||
public async export(): Promise<{
|
||||
id: string;
|
||||
domainName: string;
|
||||
created: number;
|
||||
validUntil: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
csr: string;
|
||||
} | undefined> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ExportCertificate>(
|
||||
'exportCertificate',
|
||||
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to export certificate');
|
||||
}
|
||||
return response.cert;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICertificateSummary {
|
||||
total: number;
|
||||
valid: number;
|
||||
expiring: number;
|
||||
expired: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
}
|
||||
|
||||
export class CertificateManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<{ certificates: Certificate[]; summary: ICertificateSummary }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetCertificateOverview>(
|
||||
'getCertificateOverview',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return {
|
||||
certificates: response.certificates.map((c) => new Certificate(this.clientRef, c)),
|
||||
summary: response.summary,
|
||||
};
|
||||
}
|
||||
|
||||
public async import(cert: {
|
||||
id: string;
|
||||
domainName: string;
|
||||
created: number;
|
||||
validUntil: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
csr: string;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ImportCertificate>(
|
||||
'importCertificate',
|
||||
this.clientRef.buildRequestPayload({ cert }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to import certificate');
|
||||
}
|
||||
}
|
||||
}
|
||||
17
ts_apiclient/classes.config.ts
Normal file
17
ts_apiclient/classes.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class ConfigManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async get(section?: string): Promise<interfaces.requests.IReq_GetConfiguration['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetConfiguration>(
|
||||
'getConfiguration',
|
||||
this.clientRef.buildRequestPayload({ section }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
import { RouteManager } from './classes.route.js';
|
||||
import { CertificateManager } from './classes.certificate.js';
|
||||
import { ApiTokenManager } from './classes.apitoken.js';
|
||||
import { RemoteIngressManager } from './classes.remoteingress.js';
|
||||
import { StatsManager } from './classes.stats.js';
|
||||
import { ConfigManager } from './classes.config.js';
|
||||
import { LogManager } from './classes.logs.js';
|
||||
import { EmailManager } from './classes.email.js';
|
||||
import { RadiusManager } from './classes.radius.js';
|
||||
|
||||
export interface IDcRouterApiClientOptions {
|
||||
baseUrl: string;
|
||||
apiToken?: string;
|
||||
}
|
||||
|
||||
export class DcRouterApiClient {
|
||||
public baseUrl: string;
|
||||
public apiToken?: string;
|
||||
public identity?: interfaces.data.IIdentity;
|
||||
|
||||
// Resource managers
|
||||
public routes: RouteManager;
|
||||
public certificates: CertificateManager;
|
||||
public apiTokens: ApiTokenManager;
|
||||
public remoteIngress: RemoteIngressManager;
|
||||
public stats: StatsManager;
|
||||
public config: ConfigManager;
|
||||
public logs: LogManager;
|
||||
public emails: EmailManager;
|
||||
public radius: RadiusManager;
|
||||
|
||||
constructor(options: IDcRouterApiClientOptions) {
|
||||
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
||||
this.apiToken = options.apiToken;
|
||||
|
||||
this.routes = new RouteManager(this);
|
||||
this.certificates = new CertificateManager(this);
|
||||
this.apiTokens = new ApiTokenManager(this);
|
||||
this.remoteIngress = new RemoteIngressManager(this);
|
||||
this.stats = new StatsManager(this);
|
||||
this.config = new ConfigManager(this);
|
||||
this.logs = new LogManager(this);
|
||||
this.emails = new EmailManager(this);
|
||||
this.radius = new RadiusManager(this);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Auth
|
||||
// =====================
|
||||
|
||||
public async login(username: string, password: string): Promise<interfaces.data.IIdentity> {
|
||||
const response = await this.request<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'adminLoginWithUsernameAndPassword',
|
||||
{ username, password },
|
||||
);
|
||||
if (response.identity) {
|
||||
this.identity = response.identity;
|
||||
}
|
||||
return response.identity!;
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
await this.request<interfaces.requests.IReq_AdminLogout>(
|
||||
'adminLogout',
|
||||
{ identity: this.identity! },
|
||||
);
|
||||
this.identity = undefined;
|
||||
}
|
||||
|
||||
public async verifyIdentity(): Promise<{ valid: boolean; identity?: interfaces.data.IIdentity }> {
|
||||
const response = await this.request<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'verifyIdentity',
|
||||
{ identity: this.identity! },
|
||||
);
|
||||
if (response.identity) {
|
||||
this.identity = response.identity;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Internal request helper
|
||||
// =====================
|
||||
|
||||
public async request<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
method: string,
|
||||
requestData: T['request'],
|
||||
): Promise<T['response']> {
|
||||
const typedRequest = new plugins.typedrequest.TypedRequest<T>(
|
||||
`${this.baseUrl}/typedrequest`,
|
||||
method,
|
||||
);
|
||||
return typedRequest.fire(requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a request payload with identity and optional API token auto-injected.
|
||||
*/
|
||||
public buildRequestPayload(extra: Record<string, any> = {}): Record<string, any> {
|
||||
const payload: Record<string, any> = { ...extra };
|
||||
if (this.identity) {
|
||||
payload.identity = this.identity;
|
||||
}
|
||||
if (this.apiToken) {
|
||||
payload.apiToken = this.apiToken;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
77
ts_apiclient/classes.email.ts
Normal file
77
ts_apiclient/classes.email.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Email {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IEmail
|
||||
public id: string;
|
||||
public direction: interfaces.requests.TEmailDirection;
|
||||
public status: interfaces.requests.TEmailStatus;
|
||||
public from: string;
|
||||
public to: string;
|
||||
public subject: string;
|
||||
public timestamp: string;
|
||||
public messageId: string;
|
||||
public size: string;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.IEmail) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.direction = data.direction;
|
||||
this.status = data.status;
|
||||
this.from = data.from;
|
||||
this.to = data.to;
|
||||
this.subject = data.subject;
|
||||
this.timestamp = data.timestamp;
|
||||
this.messageId = data.messageId;
|
||||
this.size = data.size;
|
||||
}
|
||||
|
||||
public async getDetail(): Promise<interfaces.requests.IEmailDetail | null> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||
'getEmailDetail',
|
||||
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||
);
|
||||
return response.email;
|
||||
}
|
||||
|
||||
public async resend(): Promise<{ success: boolean; newQueueId?: string }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||
'resendEmail',
|
||||
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<Email[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetAllEmails>(
|
||||
'getAllEmails',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.emails.map((e) => new Email(this.clientRef, e));
|
||||
}
|
||||
|
||||
public async getDetail(emailId: string): Promise<interfaces.requests.IEmailDetail | null> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||
'getEmailDetail',
|
||||
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||
);
|
||||
return response.email;
|
||||
}
|
||||
|
||||
public async resend(emailId: string): Promise<{ success: boolean; newQueueId?: string }> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||
'resendEmail',
|
||||
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
ts_apiclient/classes.logs.ts
Normal file
37
ts_apiclient/classes.logs.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class LogManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async getRecent(options?: {
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
timeRange?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetRecentLogs['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRecentLogs>(
|
||||
'getRecentLogs',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getStream(options?: {
|
||||
follow?: boolean;
|
||||
filters?: {
|
||||
level?: string[];
|
||||
category?: string[];
|
||||
};
|
||||
}): Promise<interfaces.requests.IReq_GetLogStream['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetLogStream>(
|
||||
'getLogStream',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
180
ts_apiclient/classes.radius.ts
Normal file
180
ts_apiclient/classes.radius.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
// =====================
|
||||
// Sub-managers
|
||||
// =====================
|
||||
|
||||
export class RadiusClientManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<Array<{
|
||||
name: string;
|
||||
ipRange: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}>> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusClients>(
|
||||
'getRadiusClients',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.clients;
|
||||
}
|
||||
|
||||
public async set(client: {
|
||||
name: string;
|
||||
ipRange: string;
|
||||
secret: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetRadiusClient>(
|
||||
'setRadiusClient',
|
||||
this.clientRef.buildRequestPayload({ client }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set RADIUS client');
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(name: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRadiusClient>(
|
||||
'removeRadiusClient',
|
||||
this.clientRef.buildRequestPayload({ name }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove RADIUS client');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RadiusVlanManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<interfaces.requests.IReq_GetVlanMappings['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetVlanMappings>(
|
||||
'getVlanMappings',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async set(mapping: {
|
||||
mac: string;
|
||||
vlan: number;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetVlanMapping>(
|
||||
'setVlanMapping',
|
||||
this.clientRef.buildRequestPayload({ mapping }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set VLAN mapping');
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(mac: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveVlanMapping>(
|
||||
'removeVlanMapping',
|
||||
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove VLAN mapping');
|
||||
}
|
||||
}
|
||||
|
||||
public async updateConfig(options: {
|
||||
defaultVlan?: number;
|
||||
allowUnknownMacs?: boolean;
|
||||
}): Promise<{ defaultVlan: number; allowUnknownMacs: boolean }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateVlanConfig>(
|
||||
'updateVlanConfig',
|
||||
this.clientRef.buildRequestPayload(options) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to update VLAN config');
|
||||
}
|
||||
return response.config;
|
||||
}
|
||||
|
||||
public async testAssignment(mac: string): Promise<interfaces.requests.IReq_TestVlanAssignment['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_TestVlanAssignment>(
|
||||
'testVlanAssignment',
|
||||
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RadiusSessionManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(filter?: {
|
||||
username?: string;
|
||||
nasIpAddress?: string;
|
||||
vlanId?: number;
|
||||
}): Promise<interfaces.requests.IReq_GetRadiusSessions['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRadiusSessions>(
|
||||
'getRadiusSessions',
|
||||
this.clientRef.buildRequestPayload({ filter }) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async disconnect(sessionId: string, reason?: string): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DisconnectRadiusSession>(
|
||||
'disconnectRadiusSession',
|
||||
this.clientRef.buildRequestPayload({ sessionId, reason }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to disconnect session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Main RADIUS Manager
|
||||
// =====================
|
||||
|
||||
export class RadiusManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
public clients: RadiusClientManager;
|
||||
public vlans: RadiusVlanManager;
|
||||
public sessions: RadiusSessionManager;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
this.clients = new RadiusClientManager(clientRef);
|
||||
this.vlans = new RadiusVlanManager(clientRef);
|
||||
this.sessions = new RadiusSessionManager(clientRef);
|
||||
}
|
||||
|
||||
public async getAccountingSummary(
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
): Promise<interfaces.requests.IReq_GetRadiusAccountingSummary['response']['summary']> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
||||
'getRadiusAccountingSummary',
|
||||
this.clientRef.buildRequestPayload({ startTime, endTime }) as any,
|
||||
);
|
||||
return response.summary;
|
||||
}
|
||||
|
||||
public async getStatistics(): Promise<interfaces.requests.IReq_GetRadiusStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRadiusStatistics>(
|
||||
'getRadiusStatistics',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
185
ts_apiclient/classes.remoteingress.ts
Normal file
185
ts_apiclient/classes.remoteingress.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class RemoteIngress {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IRemoteIngress
|
||||
public id: string;
|
||||
public name: string;
|
||||
public secret: string;
|
||||
public listenPorts: number[];
|
||||
public enabled: boolean;
|
||||
public autoDerivePorts: boolean;
|
||||
public tags?: string[];
|
||||
public createdAt: number;
|
||||
public updatedAt: number;
|
||||
public effectiveListenPorts?: number[];
|
||||
public manualPorts?: number[];
|
||||
public derivedPorts?: number[];
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IRemoteIngress) {
|
||||
this.clientRef = clientRef;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.secret = data.secret;
|
||||
this.listenPorts = data.listenPorts;
|
||||
this.enabled = data.enabled;
|
||||
this.autoDerivePorts = data.autoDerivePorts;
|
||||
this.tags = data.tags;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
this.effectiveListenPorts = data.effectiveListenPorts;
|
||||
this.manualPorts = data.manualPorts;
|
||||
this.derivedPorts = data.derivedPorts;
|
||||
}
|
||||
|
||||
public async update(changes: {
|
||||
name?: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
enabled?: boolean;
|
||||
tags?: string[];
|
||||
}): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRemoteIngress>(
|
||||
'updateRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({ id: this.id, ...changes }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to update remote ingress');
|
||||
}
|
||||
// Update local state from response
|
||||
const edge = response.edge;
|
||||
this.name = edge.name;
|
||||
this.listenPorts = edge.listenPorts;
|
||||
this.enabled = edge.enabled;
|
||||
this.autoDerivePorts = edge.autoDerivePorts;
|
||||
this.tags = edge.tags;
|
||||
this.updatedAt = edge.updatedAt;
|
||||
this.effectiveListenPorts = edge.effectiveListenPorts;
|
||||
this.manualPorts = edge.manualPorts;
|
||||
this.derivedPorts = edge.derivedPorts;
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRemoteIngress>(
|
||||
'deleteRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete remote ingress');
|
||||
}
|
||||
}
|
||||
|
||||
public async regenerateSecret(): Promise<string> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
||||
'regenerateRemoteIngressSecret',
|
||||
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to regenerate secret');
|
||||
}
|
||||
this.secret = response.secret;
|
||||
return response.secret;
|
||||
}
|
||||
|
||||
public async getConnectionToken(hubHost?: string): Promise<string | undefined> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||
'getRemoteIngressConnectionToken',
|
||||
this.clientRef.buildRequestPayload({ edgeId: this.id, hubHost }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to get connection token');
|
||||
}
|
||||
return response.token;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteIngressBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private edgeName: string = '';
|
||||
private edgeListenPorts?: number[];
|
||||
private edgeAutoDerivePorts?: boolean;
|
||||
private edgeTags?: string[];
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.edgeName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setListenPorts(ports: number[]): this {
|
||||
this.edgeListenPorts = ports;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAutoDerivePorts(auto: boolean): this {
|
||||
this.edgeAutoDerivePorts = auto;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setTags(tags: string[]): this {
|
||||
this.edgeTags = tags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<RemoteIngress> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRemoteIngress>(
|
||||
'createRemoteIngress',
|
||||
this.clientRef.buildRequestPayload({
|
||||
name: this.edgeName,
|
||||
listenPorts: this.edgeListenPorts,
|
||||
autoDerivePorts: this.edgeAutoDerivePorts,
|
||||
tags: this.edgeTags,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to create remote ingress');
|
||||
}
|
||||
return new RemoteIngress(this.clientRef, response.edge);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteIngressManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<RemoteIngress[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngresses>(
|
||||
'getRemoteIngresses',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.edges.map((e) => new RemoteIngress(this.clientRef, e));
|
||||
}
|
||||
|
||||
public async getStatuses(): Promise<interfaces.data.IRemoteIngressStatus[]> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
||||
'getRemoteIngressStatus',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return response.statuses;
|
||||
}
|
||||
|
||||
public async create(options: {
|
||||
name: string;
|
||||
listenPorts?: number[];
|
||||
autoDerivePorts?: boolean;
|
||||
tags?: string[];
|
||||
}): Promise<RemoteIngress> {
|
||||
const builder = this.build().setName(options.name);
|
||||
if (options.listenPorts) builder.setListenPorts(options.listenPorts);
|
||||
if (options.autoDerivePorts !== undefined) builder.setAutoDerivePorts(options.autoDerivePorts);
|
||||
if (options.tags) builder.setTags(options.tags);
|
||||
return builder.save();
|
||||
}
|
||||
|
||||
public build(): RemoteIngressBuilder {
|
||||
return new RemoteIngressBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
203
ts_apiclient/classes.route.ts
Normal file
203
ts_apiclient/classes.route.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
export class Route {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
// Data from IMergedRoute
|
||||
public routeConfig: IRouteConfig;
|
||||
public source: 'hardcoded' | 'programmatic';
|
||||
public enabled: boolean;
|
||||
public overridden: boolean;
|
||||
public storedRouteId?: string;
|
||||
public createdAt?: number;
|
||||
public updatedAt?: number;
|
||||
|
||||
// Convenience accessors
|
||||
public get name(): string {
|
||||
return this.routeConfig.name || '';
|
||||
}
|
||||
|
||||
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IMergedRoute) {
|
||||
this.clientRef = clientRef;
|
||||
this.routeConfig = data.route;
|
||||
this.source = data.source;
|
||||
this.enabled = data.enabled;
|
||||
this.overridden = data.overridden;
|
||||
this.storedRouteId = data.storedRouteId;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
|
||||
public async update(changes: Partial<IRouteConfig>): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot update a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRoute>(
|
||||
'updateRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId, route: changes }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to update route');
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot delete a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRoute>(
|
||||
'deleteRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to delete route');
|
||||
}
|
||||
}
|
||||
|
||||
public async toggle(enabled: boolean): Promise<void> {
|
||||
if (!this.storedRouteId) {
|
||||
throw new Error('Cannot toggle a hardcoded route. Use setOverride() instead.');
|
||||
}
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleRoute>(
|
||||
'toggleRoute',
|
||||
this.clientRef.buildRequestPayload({ id: this.storedRouteId, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to toggle route');
|
||||
}
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public async setOverride(enabled: boolean): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_SetRouteOverride>(
|
||||
'setRouteOverride',
|
||||
this.clientRef.buildRequestPayload({ routeName: this.name, enabled }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to set route override');
|
||||
}
|
||||
this.overridden = true;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public async removeOverride(): Promise<void> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRouteOverride>(
|
||||
'removeRouteOverride',
|
||||
this.clientRef.buildRequestPayload({ routeName: this.name }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to remove route override');
|
||||
}
|
||||
this.overridden = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteBuilder {
|
||||
private clientRef: DcRouterApiClient;
|
||||
private routeConfig: Partial<IRouteConfig> = {};
|
||||
private isEnabled: boolean = true;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public setName(name: string): this {
|
||||
this.routeConfig.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMatch(match: IRouteConfig['match']): this {
|
||||
this.routeConfig.match = match;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAction(action: IRouteConfig['action']): this {
|
||||
this.routeConfig.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setTls(tls: IRouteConfig['action']['tls']): this {
|
||||
if (!this.routeConfig.action) {
|
||||
this.routeConfig.action = { type: 'forward' } as IRouteConfig['action'];
|
||||
}
|
||||
this.routeConfig.action!.tls = tls;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): this {
|
||||
this.isEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<Route> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||
'createRoute',
|
||||
this.clientRef.buildRequestPayload({
|
||||
route: this.routeConfig as IRouteConfig,
|
||||
enabled: this.isEnabled,
|
||||
}) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create route');
|
||||
}
|
||||
|
||||
// Return a Route instance by re-fetching the list
|
||||
// The created route is programmatic, so we find it by storedRouteId
|
||||
const { routes } = await new RouteManager(this.clientRef).list();
|
||||
const created = routes.find((r) => r.storedRouteId === response.storedRouteId);
|
||||
if (created) {
|
||||
return created;
|
||||
}
|
||||
|
||||
// Fallback: construct from known data
|
||||
return new Route(this.clientRef, {
|
||||
route: this.routeConfig as IRouteConfig,
|
||||
source: 'programmatic',
|
||||
enabled: this.isEnabled,
|
||||
overridden: false,
|
||||
storedRouteId: response.storedRouteId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async list(): Promise<{ routes: Route[]; warnings: interfaces.data.IRouteWarning[] }> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_GetMergedRoutes>(
|
||||
'getMergedRoutes',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
return {
|
||||
routes: response.routes.map((r) => new Route(this.clientRef, r)),
|
||||
warnings: response.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
public async create(routeConfig: IRouteConfig, enabled?: boolean): Promise<Route> {
|
||||
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||
'createRoute',
|
||||
this.clientRef.buildRequestPayload({ route: routeConfig, enabled: enabled ?? true }) as any,
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Failed to create route');
|
||||
}
|
||||
return new Route(this.clientRef, {
|
||||
route: routeConfig,
|
||||
source: 'programmatic',
|
||||
enabled: enabled ?? true,
|
||||
overridden: false,
|
||||
storedRouteId: response.storedRouteId,
|
||||
});
|
||||
}
|
||||
|
||||
public build(): RouteBuilder {
|
||||
return new RouteBuilder(this.clientRef);
|
||||
}
|
||||
}
|
||||
111
ts_apiclient/classes.stats.ts
Normal file
111
ts_apiclient/classes.stats.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||
|
||||
type TTimeRange = '1h' | '6h' | '24h' | '7d' | '30d';
|
||||
|
||||
export class StatsManager {
|
||||
private clientRef: DcRouterApiClient;
|
||||
|
||||
constructor(clientRef: DcRouterApiClient) {
|
||||
this.clientRef = clientRef;
|
||||
}
|
||||
|
||||
public async getServer(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
includeHistory?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetServerStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetServerStatistics>(
|
||||
'getServerStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getEmail(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
domain?: string;
|
||||
includeDetails?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetEmailStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetEmailStatistics>(
|
||||
'getEmailStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getDns(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
domain?: string;
|
||||
includeQueryTypes?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetDnsStatistics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetDnsStatistics>(
|
||||
'getDnsStatistics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getRateLimits(options?: {
|
||||
domain?: string;
|
||||
ip?: string;
|
||||
includeBlocked?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetRateLimitStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||
'getRateLimitStatus',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getSecurity(options?: {
|
||||
timeRange?: TTimeRange;
|
||||
includeDetails?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetSecurityMetrics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetSecurityMetrics>(
|
||||
'getSecurityMetrics',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getConnections(options?: {
|
||||
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
|
||||
state?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetActiveConnections['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetActiveConnections>(
|
||||
'getActiveConnections',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getQueues(options?: {
|
||||
queueName?: string;
|
||||
}): Promise<interfaces.requests.IReq_GetQueueStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetQueueStatus>(
|
||||
'getQueueStatus',
|
||||
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getHealth(detailed?: boolean): Promise<interfaces.requests.IReq_GetHealthStatus['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'getHealthStatus',
|
||||
this.clientRef.buildRequestPayload({ detailed }) as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getNetwork(): Promise<interfaces.requests.IReq_GetNetworkStats['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetNetworkStats>(
|
||||
'getNetworkStats',
|
||||
this.clientRef.buildRequestPayload() as any,
|
||||
);
|
||||
}
|
||||
|
||||
public async getCombined(sections?: {
|
||||
server?: boolean;
|
||||
email?: boolean;
|
||||
dns?: boolean;
|
||||
security?: boolean;
|
||||
network?: boolean;
|
||||
}): Promise<interfaces.requests.IReq_GetCombinedMetrics['response']> {
|
||||
return this.clientRef.request<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||
'getCombinedMetrics',
|
||||
this.clientRef.buildRequestPayload({ sections }) as any,
|
||||
);
|
||||
}
|
||||
}
|
||||
15
ts_apiclient/index.ts
Normal file
15
ts_apiclient/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Main client
|
||||
export { DcRouterApiClient, type IDcRouterApiClientOptions } from './classes.dcrouterapiclient.js';
|
||||
|
||||
// Resource classes
|
||||
export { Route, RouteBuilder, RouteManager } from './classes.route.js';
|
||||
export { Certificate, CertificateManager, type ICertificateSummary } from './classes.certificate.js';
|
||||
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
|
||||
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
|
||||
export { Email, EmailManager } from './classes.email.js';
|
||||
|
||||
// Read-only managers
|
||||
export { StatsManager } from './classes.stats.js';
|
||||
export { ConfigManager } from './classes.config.js';
|
||||
export { LogManager } from './classes.logs.js';
|
||||
export { RadiusManager, RadiusClientManager, RadiusVlanManager, RadiusSessionManager } from './classes.radius.js';
|
||||
8
ts_apiclient/plugins.ts
Normal file
8
ts_apiclient/plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// @api.global scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedrequestInterfaces,
|
||||
};
|
||||
279
ts_apiclient/readme.md
Normal file
279
ts_apiclient/readme.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# @serve.zone/dcrouter-apiclient
|
||||
|
||||
A typed, object-oriented API client for DcRouter with a fluent builder pattern. 🔧
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @serve.zone/dcrouter-apiclient
|
||||
```
|
||||
|
||||
Or import directly from the main package:
|
||||
|
||||
```typescript
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||
|
||||
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`);
|
||||
|
||||
// Check health
|
||||
const { health } = await client.stats.getHealth();
|
||||
console.log(`Healthy: ${health.healthy}`);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 🔐 Authentication
|
||||
|
||||
```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
|
||||
|
||||
Routes are returned as `Route` instances with methods for update, delete, toggle, and overrides:
|
||||
|
||||
```typescript
|
||||
// List all routes (hardcoded + programmatic)
|
||||
const { routes, warnings } = 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();
|
||||
```
|
||||
|
||||
**Builder pattern** for creating new routes:
|
||||
|
||||
```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' })
|
||||
.setEnabled(true)
|
||||
.save();
|
||||
|
||||
// Or use quick creation
|
||||
const route = await client.routes.create(routeConfig);
|
||||
```
|
||||
|
||||
### 🔑 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
|
||||
|
||||
```typescript
|
||||
const { certificates, summary } = await client.certificates.list();
|
||||
console.log(`${summary.valid} valid, ${summary.expiring} expiring, ${summary.failed} 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: '...',
|
||||
});
|
||||
```
|
||||
|
||||
### 🌍 Remote Ingress
|
||||
|
||||
```typescript
|
||||
// List edges and their statuses
|
||||
const edges = await client.remoteIngress.list();
|
||||
const statuses = await client.remoteIngress.getStatuses();
|
||||
|
||||
// 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.
|
||||
|
||||
## 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.
|
||||
3
ts_apiclient/tspublish.json
Normal file
3
ts_apiclient/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"order": 4
|
||||
}
|
||||
@@ -82,6 +82,14 @@ interface IIdentity {
|
||||
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
||||
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
||||
|
||||
#### 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` |
|
||||
|
||||
#### Remote Ingress Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
@@ -128,13 +136,29 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
||||
#### 📧 Email Operations
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetQueuedEmails` | `getQueuedEmails` | List queued emails |
|
||||
| `IReq_GetSentEmails` | `getSentEmails` | List delivered emails |
|
||||
| `IReq_GetFailedEmails` | `getFailedEmails` | List failed emails |
|
||||
| `IReq_GetAllEmails` | `getAllEmails` | List all emails |
|
||||
| `IReq_GetEmailDetail` | `getEmailDetail` | Full detail for a specific email |
|
||||
| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email |
|
||||
| `IReq_GetSecurityIncidents` | `getSecurityIncidents` | Security events |
|
||||
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
|
||||
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
|
||||
|
||||
#### 🛣️ 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 |
|
||||
@@ -198,6 +222,8 @@ interface ICertificateInfo {
|
||||
|
||||
## 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';
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '11.0.48',
|
||||
version: '11.2.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- Filter by log level (error, warning, info, debug)
|
||||
- Search and time-range selection
|
||||
|
||||
### 🛣️ 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
|
||||
|
||||
### ⚙️ Configuration
|
||||
- Read-only display of current system configuration
|
||||
- Status badges for boolean values (enabled/disabled)
|
||||
@@ -96,6 +101,7 @@ ts_web/
|
||||
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||
├── ops-view-remoteingress.ts # Remote ingress edge 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/
|
||||
@@ -171,6 +177,7 @@ fetchConnectionToken(edgeId) // Get connection token (standalone function)
|
||||
/emails/security → Security incidents
|
||||
/certificates → Certificate management
|
||||
/remoteingress → Remote ingress edge management
|
||||
/routes → Route & API token management
|
||||
/logs → Log viewer
|
||||
/configuration → System configuration
|
||||
/security → Security dashboard
|
||||
|
||||
Reference in New Issue
Block a user