@serve.zone/dcrouter 13.17.9 → 13.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +6 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -5
- package/dist_ts/classes.dcrouter.js +152 -120
- package/dist_ts/config/classes.route-config-manager.d.ts +13 -5
- package/dist_ts/config/classes.route-config-manager.js +76 -36
- package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
- package/dist_ts/db/documents/classes.route.doc.js +11 -2
- package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-domain.manager.js +118 -55
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +13 -0
- package/dist_ts/email/classes.smartmta-storage-manager.js +101 -0
- package/dist_ts/email/email-dns-records.d.ts +14 -0
- package/dist_ts/email/email-dns-records.js +34 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.js +6 -15
- package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
- package/dist_ts/opsserver/handlers/stats.handler.js +41 -7
- package/dist_ts_interfaces/data/route-management.d.ts +2 -0
- package/dist_ts_migrations/index.js +25 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +13 -4
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-routes.js +44 -21
- package/package.json +2 -2
- package/readme.md +190 -1543
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +190 -138
- package/ts/config/classes.route-config-manager.ts +97 -42
- package/ts/db/documents/classes.route.doc.ts +7 -0
- package/ts/email/classes.email-domain.manager.ts +136 -51
- package/ts/email/classes.smartmta-storage-manager.ts +108 -0
- package/ts/email/email-dns-records.ts +53 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +5 -19
- package/ts/opsserver/handlers/route-management.handler.ts +4 -6
- package/ts/opsserver/handlers/stats.handler.ts +43 -7
- package/ts_apiclient/readme.md +69 -195
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +16 -4
- package/ts_web/elements/network/ops-view-routes.ts +47 -29
- package/ts_web/readme.md +41 -242
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IStorageManagerLike } from '@push.rocks/smartmta';
|
|
3
|
+
|
|
4
|
+
export class SmartMtaStorageManager implements IStorageManagerLike {
|
|
5
|
+
private readonly resolvedRootDir: string;
|
|
6
|
+
|
|
7
|
+
constructor(private rootDir: string) {
|
|
8
|
+
this.resolvedRootDir = plugins.path.resolve(rootDir);
|
|
9
|
+
plugins.fsUtils.ensureDirSync(this.resolvedRootDir);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private normalizeKey(key: string): string {
|
|
13
|
+
return key.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private resolvePathForKey(key: string): string {
|
|
17
|
+
const normalizedKey = this.normalizeKey(key);
|
|
18
|
+
const resolvedPath = plugins.path.resolve(this.resolvedRootDir, normalizedKey);
|
|
19
|
+
if (
|
|
20
|
+
resolvedPath !== this.resolvedRootDir
|
|
21
|
+
&& !resolvedPath.startsWith(`${this.resolvedRootDir}${plugins.path.sep}`)
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`Storage key escapes root directory: ${key}`);
|
|
24
|
+
}
|
|
25
|
+
return resolvedPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private toStorageKey(filePath: string): string {
|
|
29
|
+
const relativePath = plugins.path.relative(this.resolvedRootDir, filePath).split(plugins.path.sep).join('/');
|
|
30
|
+
return `/${relativePath}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async get(key: string): Promise<string | null> {
|
|
34
|
+
const filePath = this.resolvePathForKey(key);
|
|
35
|
+
try {
|
|
36
|
+
return await plugins.fs.promises.readFile(filePath, 'utf8');
|
|
37
|
+
} catch (error: unknown) {
|
|
38
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async set(key: string, value: string): Promise<void> {
|
|
46
|
+
const filePath = this.resolvePathForKey(key);
|
|
47
|
+
await plugins.fs.promises.mkdir(plugins.path.dirname(filePath), { recursive: true });
|
|
48
|
+
await plugins.fs.promises.writeFile(filePath, value, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async list(prefix: string): Promise<string[]> {
|
|
52
|
+
const prefixPath = this.resolvePathForKey(prefix);
|
|
53
|
+
try {
|
|
54
|
+
const stat = await plugins.fs.promises.stat(prefixPath);
|
|
55
|
+
if (stat.isFile()) {
|
|
56
|
+
return [this.toStorageKey(prefixPath)];
|
|
57
|
+
}
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results: string[] = [];
|
|
66
|
+
const walk = async (currentPath: string): Promise<void> => {
|
|
67
|
+
const entries = await plugins.fs.promises.readdir(currentPath, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const entryPath = plugins.path.join(currentPath, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
await walk(entryPath);
|
|
72
|
+
} else if (entry.isFile()) {
|
|
73
|
+
results.push(this.toStorageKey(entryPath));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await walk(prefixPath);
|
|
79
|
+
return results.sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async delete(key: string): Promise<void> {
|
|
83
|
+
const targetPath = this.resolvePathForKey(key);
|
|
84
|
+
try {
|
|
85
|
+
const stat = await plugins.fs.promises.stat(targetPath);
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
await plugins.fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
88
|
+
} else {
|
|
89
|
+
await plugins.fs.promises.unlink(targetPath);
|
|
90
|
+
}
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let currentDir = plugins.path.dirname(targetPath);
|
|
99
|
+
while (currentDir.startsWith(this.resolvedRootDir) && currentDir !== this.resolvedRootDir) {
|
|
100
|
+
const entries = await plugins.fs.promises.readdir(currentDir);
|
|
101
|
+
if (entries.length > 0) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
await plugins.fs.promises.rmdir(currentDir);
|
|
105
|
+
currentDir = plugins.path.dirname(currentDir);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IEmailDnsRecord,
|
|
3
|
+
TDnsRecordStatus,
|
|
4
|
+
} from '../../ts_interfaces/data/email-domain.js';
|
|
5
|
+
|
|
6
|
+
type TEmailDnsStatusKey = 'mx' | 'spf' | 'dkim' | 'dmarc';
|
|
7
|
+
|
|
8
|
+
export interface IBuildEmailDnsRecordsOptions {
|
|
9
|
+
domain: string;
|
|
10
|
+
hostname: string;
|
|
11
|
+
selector?: string;
|
|
12
|
+
dkimValue?: string;
|
|
13
|
+
mxPriority?: number;
|
|
14
|
+
dmarcPolicy?: string;
|
|
15
|
+
dmarcRua?: string;
|
|
16
|
+
statuses?: Partial<Record<TEmailDnsStatusKey, TDnsRecordStatus>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildEmailDnsRecords(options: IBuildEmailDnsRecordsOptions): IEmailDnsRecord[] {
|
|
20
|
+
const statusFor = (key: TEmailDnsStatusKey): TDnsRecordStatus => options.statuses?.[key] ?? 'unchecked';
|
|
21
|
+
const selector = options.selector || 'default';
|
|
22
|
+
const records: IEmailDnsRecord[] = [
|
|
23
|
+
{
|
|
24
|
+
type: 'MX',
|
|
25
|
+
name: options.domain,
|
|
26
|
+
value: `${options.mxPriority ?? 10} ${options.hostname}`,
|
|
27
|
+
status: statusFor('mx'),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'TXT',
|
|
31
|
+
name: options.domain,
|
|
32
|
+
value: 'v=spf1 a mx ~all',
|
|
33
|
+
status: statusFor('spf'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'TXT',
|
|
37
|
+
name: `_dmarc.${options.domain}`,
|
|
38
|
+
value: `v=DMARC1; p=${options.dmarcPolicy ?? 'none'}; rua=mailto:${options.dmarcRua ?? `dmarc@${options.domain}`}`,
|
|
39
|
+
status: statusFor('dmarc'),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
if (options.dkimValue) {
|
|
44
|
+
records.splice(2, 0, {
|
|
45
|
+
type: 'TXT',
|
|
46
|
+
name: `${selector}._domainkey.${options.domain}`,
|
|
47
|
+
value: options.dkimValue,
|
|
48
|
+
status: statusFor('dkim'),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return records;
|
|
53
|
+
}
|
package/ts/email/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ export class EmailOpsHandler {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const queue = emailServer.deliveryQueue;
|
|
51
|
-
const item =
|
|
51
|
+
const item = emailServer.getQueueItem(dataArg.emailId);
|
|
52
52
|
|
|
53
53
|
if (!item) {
|
|
54
54
|
return { success: false, error: 'Email not found in queue' };
|
|
@@ -82,22 +82,10 @@ export class EmailOpsHandler {
|
|
|
82
82
|
*/
|
|
83
83
|
private getAllQueueEmails(): interfaces.requests.IEmail[] {
|
|
84
84
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
85
|
-
if (!emailServer
|
|
85
|
+
if (!emailServer) {
|
|
86
86
|
return [];
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
const queue = emailServer.deliveryQueue;
|
|
90
|
-
const queueMap = (queue as any).queue as Map<string, any>;
|
|
91
|
-
|
|
92
|
-
if (!queueMap) {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const emails: interfaces.requests.IEmail[] = [];
|
|
97
|
-
|
|
98
|
-
for (const [id, item] of queueMap.entries()) {
|
|
99
|
-
emails.push(this.mapQueueItemToEmail(item));
|
|
100
|
-
}
|
|
88
|
+
const emails = emailServer.getQueueItems().map((item) => this.mapQueueItemToEmail(item));
|
|
101
89
|
|
|
102
90
|
// Sort by createdAt descending (newest first)
|
|
103
91
|
emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
@@ -110,12 +98,10 @@ export class EmailOpsHandler {
|
|
|
110
98
|
*/
|
|
111
99
|
private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null {
|
|
112
100
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
113
|
-
if (!emailServer
|
|
101
|
+
if (!emailServer) {
|
|
114
102
|
return null;
|
|
115
103
|
}
|
|
116
|
-
|
|
117
|
-
const queue = emailServer.deliveryQueue;
|
|
118
|
-
const item = queue.getItem(emailId);
|
|
104
|
+
const item = emailServer.getQueueItem(emailId);
|
|
119
105
|
|
|
120
106
|
if (!item) {
|
|
121
107
|
return null;
|
|
@@ -87,12 +87,12 @@ export class RouteManagementHandler {
|
|
|
87
87
|
if (!manager) {
|
|
88
88
|
return { success: false, message: 'Route management not initialized' };
|
|
89
89
|
}
|
|
90
|
-
const
|
|
90
|
+
const result = await manager.updateRoute(dataArg.id, {
|
|
91
91
|
route: dataArg.route as any,
|
|
92
92
|
enabled: dataArg.enabled,
|
|
93
93
|
metadata: dataArg.metadata,
|
|
94
94
|
});
|
|
95
|
-
return
|
|
95
|
+
return result;
|
|
96
96
|
},
|
|
97
97
|
),
|
|
98
98
|
);
|
|
@@ -107,8 +107,7 @@ export class RouteManagementHandler {
|
|
|
107
107
|
if (!manager) {
|
|
108
108
|
return { success: false, message: 'Route management not initialized' };
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
return { success: ok, message: ok ? undefined : 'Route not found' };
|
|
110
|
+
return manager.deleteRoute(dataArg.id);
|
|
112
111
|
},
|
|
113
112
|
),
|
|
114
113
|
);
|
|
@@ -123,8 +122,7 @@ export class RouteManagementHandler {
|
|
|
123
122
|
if (!manager) {
|
|
124
123
|
return { success: false, message: 'Route management not initialized' };
|
|
125
124
|
}
|
|
126
|
-
|
|
127
|
-
return { success: ok, message: ok ? undefined : 'Route not found' };
|
|
125
|
+
return manager.toggleRoute(dataArg.id, dataArg.enabled);
|
|
128
126
|
},
|
|
129
127
|
),
|
|
130
128
|
);
|
|
@@ -530,13 +530,49 @@ export class StatsHandler {
|
|
|
530
530
|
nextRetry?: number;
|
|
531
531
|
}>;
|
|
532
532
|
}> {
|
|
533
|
-
|
|
533
|
+
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
534
|
+
if (!emailServer) {
|
|
535
|
+
return {
|
|
536
|
+
pending: 0,
|
|
537
|
+
active: 0,
|
|
538
|
+
failed: 0,
|
|
539
|
+
retrying: 0,
|
|
540
|
+
items: [],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const queueStats = emailServer.getQueueStats();
|
|
545
|
+
const items = emailServer.getQueueItems()
|
|
546
|
+
.sort((a, b) => {
|
|
547
|
+
const left = a.createdAt instanceof Date ? a.createdAt.getTime() : new Date(a.createdAt).getTime();
|
|
548
|
+
const right = b.createdAt instanceof Date ? b.createdAt.getTime() : new Date(b.createdAt).getTime();
|
|
549
|
+
return right - left;
|
|
550
|
+
})
|
|
551
|
+
.slice(0, 50)
|
|
552
|
+
.map((item) => {
|
|
553
|
+
const emailLike = item.processingResult;
|
|
554
|
+
const recipients = Array.isArray(emailLike?.to)
|
|
555
|
+
? emailLike.to
|
|
556
|
+
: Array.isArray(emailLike?.email?.to)
|
|
557
|
+
? emailLike.email.to
|
|
558
|
+
: [];
|
|
559
|
+
const subject = emailLike?.subject || emailLike?.email?.subject || '';
|
|
560
|
+
return {
|
|
561
|
+
id: item.id,
|
|
562
|
+
recipient: recipients[0] || '',
|
|
563
|
+
subject,
|
|
564
|
+
status: item.status,
|
|
565
|
+
attempts: item.attempts,
|
|
566
|
+
nextRetry: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : undefined,
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
|
|
534
570
|
return {
|
|
535
|
-
pending:
|
|
536
|
-
active:
|
|
537
|
-
failed:
|
|
538
|
-
retrying:
|
|
539
|
-
items
|
|
571
|
+
pending: queueStats.status.pending,
|
|
572
|
+
active: queueStats.status.processing,
|
|
573
|
+
failed: queueStats.status.failed,
|
|
574
|
+
retrying: queueStats.status.deferred,
|
|
575
|
+
items,
|
|
540
576
|
};
|
|
541
577
|
}
|
|
542
578
|
|
|
@@ -600,4 +636,4 @@ export class StatsHandler {
|
|
|
600
636
|
],
|
|
601
637
|
};
|
|
602
638
|
}
|
|
603
|
-
}
|
|
639
|
+
}
|
package/ts_apiclient/readme.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @serve.zone/dcrouter-apiclient
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, object-oriented API client for operating a running dcrouter instance. 🔧
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use this package when you want a clean TypeScript client instead of manually firing TypedRequest calls. It wraps the OpsServer API in resource managers and resource classes such as routes, certificates, tokens, edges, emails, stats, logs, config, and RADIUS.
|
|
6
6
|
|
|
7
7
|
## Issue Reporting and Security
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
14
14
|
pnpm add @serve.zone/dcrouter-apiclient
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Or import
|
|
17
|
+
Or import through the main package:
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
|
@@ -23,239 +23,113 @@ import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import { DcRouterApiClient } from '@serve.zone/dcrouter
|
|
26
|
+
import { DcRouterApiClient } from '@serve.zone/dcrouter-apiclient';
|
|
27
27
|
|
|
28
|
-
const client = new DcRouterApiClient({
|
|
28
|
+
const client = new DcRouterApiClient({
|
|
29
|
+
baseUrl: 'https://dcrouter.example.com',
|
|
30
|
+
});
|
|
29
31
|
|
|
30
|
-
// Authenticate
|
|
31
32
|
await client.login('admin', 'password');
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(`${routes.length} routes, ${warnings.length} warnings`);
|
|
34
|
+
const { routes } = await client.routes.list();
|
|
35
|
+
console.log(routes.map((route) => `${route.origin}:${route.name}`));
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
await client.routes.build()
|
|
38
|
+
.setName('api-gateway')
|
|
39
|
+
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
|
40
|
+
.setAction({ type: 'forward', targets: [{ host: '127.0.0.1', port: 8080 }] })
|
|
41
|
+
.save();
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
##
|
|
44
|
+
## Authentication Modes
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
| Mode | How it works |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Admin login | Call `login(username, password)` and the client stores the returned identity for later requests |
|
|
49
|
+
| API token | Pass `apiToken` into the constructor for token-based automation |
|
|
45
50
|
|
|
46
51
|
```typescript
|
|
47
|
-
// Login with credentials — identity is stored and auto-injected into all subsequent requests
|
|
48
|
-
const identity = await client.login('admin', 'password');
|
|
49
|
-
|
|
50
|
-
// Verify current session
|
|
51
|
-
const { valid } = await client.verifyIdentity();
|
|
52
|
-
|
|
53
|
-
// Logout
|
|
54
|
-
await client.logout();
|
|
55
|
-
|
|
56
|
-
// Or use an API token for programmatic access (route management only)
|
|
57
52
|
const client = new DcRouterApiClient({
|
|
58
53
|
baseUrl: 'https://dcrouter.example.com',
|
|
59
54
|
apiToken: 'dcr_your_token_here',
|
|
60
55
|
});
|
|
61
56
|
```
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
## Main Managers
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
| Manager | Purpose |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `client.routes` | List routes and create API-managed routes |
|
|
63
|
+
| `client.certificates` | Inspect and operate on certificate records |
|
|
64
|
+
| `client.apiTokens` | Create, list, toggle, roll, revoke API tokens |
|
|
65
|
+
| `client.remoteIngress` | Manage registered remote ingress edges |
|
|
66
|
+
| `client.stats` | Read operational metrics and health data |
|
|
67
|
+
| `client.config` | Read current configuration view |
|
|
68
|
+
| `client.logs` | Read recent logs or stream them |
|
|
69
|
+
| `client.emails` | List emails and trigger resend flows |
|
|
70
|
+
| `client.radius` | Operate on RADIUS clients, VLANs, sessions, and accounting |
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
// List all routes (hardcoded + programmatic)
|
|
69
|
-
const { routes, warnings } = await client.routes.list();
|
|
72
|
+
## Route Behavior
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
const route = routes[0];
|
|
73
|
-
console.log(route.name, route.source, route.enabled);
|
|
74
|
+
Routes are returned as `Route` instances with:
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
- `id`
|
|
77
|
+
- `name`
|
|
78
|
+
- `enabled`
|
|
79
|
+
- `origin`
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
const hardcodedRoute = routes.find(r => r.source === 'hardcoded');
|
|
82
|
-
await hardcodedRoute.setOverride(false);
|
|
83
|
-
await hardcodedRoute.removeOverride();
|
|
84
|
-
```
|
|
81
|
+
Important behavior:
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
- API routes can be created, updated, deleted, and toggled.
|
|
84
|
+
- System routes can be listed and toggled, but not edited or deleted.
|
|
85
|
+
- A system route is any route whose `origin !== 'api'`.
|
|
87
86
|
|
|
88
87
|
```typescript
|
|
89
|
-
const
|
|
90
|
-
.setName('api-gateway')
|
|
91
|
-
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
|
92
|
-
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
|
93
|
-
.setTls({ mode: 'terminate', certificate: 'auto' })
|
|
94
|
-
.setEnabled(true)
|
|
95
|
-
.save();
|
|
88
|
+
const { routes } = await client.routes.list();
|
|
96
89
|
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
for (const route of routes) {
|
|
91
|
+
if (route.origin !== 'api') {
|
|
92
|
+
await route.toggle(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
99
95
|
```
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
## Builder Example
|
|
102
98
|
|
|
103
99
|
```typescript
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
const route = await client.routes.build()
|
|
101
|
+
.setName('internal-app')
|
|
102
|
+
.setMatch({
|
|
103
|
+
ports: 80,
|
|
104
|
+
domains: ['internal.example.com'],
|
|
105
|
+
})
|
|
106
|
+
.setAction({
|
|
107
|
+
type: 'forward',
|
|
108
|
+
targets: [{ host: '127.0.0.1', port: 3000 }],
|
|
109
|
+
})
|
|
110
|
+
.setEnabled(true)
|
|
113
111
|
.save();
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Manage tokens
|
|
118
|
-
await token.toggle(false); // Disable
|
|
119
|
-
const newValue = await token.roll(); // Regenerate secret
|
|
120
|
-
await token.revoke(); // Delete
|
|
113
|
+
await route.toggle(false);
|
|
121
114
|
```
|
|
122
115
|
|
|
123
|
-
|
|
116
|
+
## Example: Certificates and Stats
|
|
124
117
|
|
|
125
118
|
```typescript
|
|
126
119
|
const { certificates, summary } = await client.certificates.list();
|
|
127
|
-
console.log(
|
|
128
|
-
|
|
129
|
-
// Operate on individual certificates
|
|
130
|
-
const cert = certificates[0];
|
|
131
|
-
await cert.reprovision();
|
|
132
|
-
const exported = await cert.export();
|
|
133
|
-
await cert.delete();
|
|
134
|
-
|
|
135
|
-
// Import a certificate
|
|
136
|
-
await client.certificates.import({
|
|
137
|
-
id: 'cert-id',
|
|
138
|
-
domainName: 'example.com',
|
|
139
|
-
created: Date.now(),
|
|
140
|
-
validUntil: Date.now() + 90 * 24 * 3600 * 1000,
|
|
141
|
-
privateKey: '...',
|
|
142
|
-
publicKey: '...',
|
|
143
|
-
csr: '...',
|
|
144
|
-
});
|
|
145
|
-
```
|
|
120
|
+
console.log(summary.valid, summary.failed);
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// List edges and their statuses
|
|
151
|
-
const edges = await client.remoteIngress.list();
|
|
152
|
-
const statuses = await client.remoteIngress.getStatuses();
|
|
153
|
-
|
|
154
|
-
// Create with builder
|
|
155
|
-
const edge = await client.remoteIngress.build()
|
|
156
|
-
.setName('edge-nyc-01')
|
|
157
|
-
.setListenPorts([80, 443])
|
|
158
|
-
.setAutoDerivePorts(true)
|
|
159
|
-
.setTags(['us-east'])
|
|
160
|
-
.save();
|
|
161
|
-
|
|
162
|
-
// Manage an edge
|
|
163
|
-
await edge.update({ name: 'edge-nyc-02' });
|
|
164
|
-
const newSecret = await edge.regenerateSecret();
|
|
165
|
-
const token = await edge.getConnectionToken();
|
|
166
|
-
await edge.delete();
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### 📊 Statistics (Read-Only)
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
const serverStats = await client.stats.getServer({ timeRange: '24h', includeHistory: true });
|
|
173
|
-
const emailStats = await client.stats.getEmail({ domain: 'example.com' });
|
|
174
|
-
const dnsStats = await client.stats.getDns();
|
|
175
|
-
const security = await client.stats.getSecurity({ includeDetails: true });
|
|
176
|
-
const connections = await client.stats.getConnections({ protocol: 'https' });
|
|
177
|
-
const queues = await client.stats.getQueues();
|
|
178
|
-
const health = await client.stats.getHealth(true);
|
|
179
|
-
const network = await client.stats.getNetwork();
|
|
180
|
-
const combined = await client.stats.getCombined({ server: true, email: true });
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### ⚙️ Configuration & Logs
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// Read-only configuration
|
|
187
|
-
const config = await client.config.get();
|
|
188
|
-
const emailSection = await client.config.get('email');
|
|
189
|
-
|
|
190
|
-
// Logs
|
|
191
|
-
const { logs, total, hasMore } = await client.logs.getRecent({
|
|
192
|
-
level: 'error',
|
|
193
|
-
category: 'smtp',
|
|
194
|
-
limit: 50,
|
|
195
|
-
});
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### 📧 Email Operations
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
const emails = await client.emails.list();
|
|
202
|
-
const email = emails[0];
|
|
203
|
-
const detail = await email.getDetail();
|
|
204
|
-
await email.resend();
|
|
205
|
-
|
|
206
|
-
// Or use the manager directly
|
|
207
|
-
const detail2 = await client.emails.getDetail('email-id');
|
|
208
|
-
await client.emails.resend('email-id');
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### 📡 RADIUS
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// Client management
|
|
215
|
-
const clients = await client.radius.clients.list();
|
|
216
|
-
await client.radius.clients.set({
|
|
217
|
-
name: 'switch-1',
|
|
218
|
-
ipRange: '192.168.1.0/24',
|
|
219
|
-
secret: 'shared-secret',
|
|
220
|
-
enabled: true,
|
|
221
|
-
});
|
|
222
|
-
await client.radius.clients.remove('switch-1');
|
|
223
|
-
|
|
224
|
-
// VLAN management
|
|
225
|
-
const { mappings, config: vlanConfig } = await client.radius.vlans.list();
|
|
226
|
-
await client.radius.vlans.set({ mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true });
|
|
227
|
-
const result = await client.radius.vlans.testAssignment('aa:bb:cc:dd:ee:ff');
|
|
228
|
-
await client.radius.vlans.updateConfig({ defaultVlan: 200 });
|
|
229
|
-
|
|
230
|
-
// Sessions
|
|
231
|
-
const { sessions } = await client.radius.sessions.list({ vlanId: 10 });
|
|
232
|
-
await client.radius.sessions.disconnect('session-id', 'Admin disconnect');
|
|
233
|
-
|
|
234
|
-
// Statistics & Accounting
|
|
235
|
-
const stats = await client.radius.getStatistics();
|
|
236
|
-
const summary = await client.radius.getAccountingSummary(startTime, endTime);
|
|
122
|
+
const health = await client.stats.getHealth();
|
|
123
|
+
const recentLogs = await client.logs.getRecent({ level: 'error', limit: 20 });
|
|
237
124
|
```
|
|
238
125
|
|
|
239
|
-
##
|
|
240
|
-
|
|
241
|
-
| Manager | Methods |
|
|
242
|
-
|---------|---------|
|
|
243
|
-
| `client.login()` / `logout()` / `verifyIdentity()` | Authentication |
|
|
244
|
-
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
|
|
245
|
-
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
|
|
246
|
-
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
|
|
247
|
-
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
|
|
248
|
-
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getRateLimits()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
|
|
249
|
-
| `client.config` | `get(section?)` |
|
|
250
|
-
| `client.logs` | `getRecent()`, `getStream()` |
|
|
251
|
-
| `client.emails` | `list()`, `getDetail()`, `resend()` → Email: `getDetail()`, `resend()` |
|
|
252
|
-
| `client.radius` | `.clients.list/set/remove()`, `.vlans.list/set/remove/updateConfig/testAssignment()`, `.sessions.list/disconnect()`, `getStatistics()`, `getAccountingSummary()` |
|
|
253
|
-
|
|
254
|
-
## Architecture
|
|
126
|
+
## What This Package Does Not Do
|
|
255
127
|
|
|
256
|
-
|
|
128
|
+
- It does not start dcrouter.
|
|
129
|
+
- It does not embed the dashboard.
|
|
130
|
+
- It does not replace the request interfaces package if you only need raw types.
|
|
257
131
|
|
|
258
|
-
|
|
132
|
+
Use `@serve.zone/dcrouter` to run the server, `@serve.zone/dcrouter-web` for the dashboard bundle/components, and `@serve.zone/dcrouter-interfaces` for raw API contracts.
|
|
259
133
|
|
|
260
134
|
## License and Legal Information
|
|
261
135
|
|