@serve.zone/dcrouter 13.18.0 → 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 +2 -0
- package/dist_ts/classes.dcrouter.js +50 -39
- 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.js +9 -28
- 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 +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +5 -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 -3
- package/readme.md +190 -1543
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +61 -47
- 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 +8 -28
- package/ts/email/email-dns-records.ts +53 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/handlers/route-management.handler.ts +4 -6
- 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
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -30,7 +30,8 @@ import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/
|
|
|
30
30
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
|
31
31
|
import { DnsManager } from './dns/manager.dns.js';
|
|
32
32
|
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
|
33
|
-
import { EmailDomainManager, SmartMtaStorageManager } from './email/index.js';
|
|
33
|
+
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
|
|
34
|
+
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
|
34
35
|
|
|
35
36
|
export interface IDcRouterOptions {
|
|
36
37
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
|
@@ -314,7 +315,8 @@ export class DcRouter {
|
|
|
314
315
|
// Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
|
|
315
316
|
private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
316
317
|
private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
317
|
-
|
|
318
|
+
private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
319
|
+
// Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
|
|
318
320
|
private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
319
321
|
|
|
320
322
|
// Environment access
|
|
@@ -588,13 +590,15 @@ export class DcRouter {
|
|
|
588
590
|
this.tunnelManager.syncAllowedEdges();
|
|
589
591
|
}
|
|
590
592
|
},
|
|
591
|
-
|
|
593
|
+
undefined,
|
|
594
|
+
(storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute),
|
|
592
595
|
);
|
|
593
596
|
this.apiTokenManager = new ApiTokenManager();
|
|
594
597
|
await this.apiTokenManager.initialize();
|
|
595
598
|
await this.routeConfigManager.initialize(
|
|
596
599
|
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
597
600
|
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
601
|
+
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
598
602
|
);
|
|
599
603
|
await this.targetProfileManager.normalizeAllRouteRefs();
|
|
600
604
|
|
|
@@ -912,10 +916,12 @@ export class DcRouter {
|
|
|
912
916
|
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
|
|
913
917
|
}
|
|
914
918
|
|
|
919
|
+
this.seedDnsRoutes = [];
|
|
915
920
|
this.runtimeDnsRoutes = [];
|
|
916
921
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
|
917
|
-
this.
|
|
918
|
-
|
|
922
|
+
this.seedDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: false });
|
|
923
|
+
this.runtimeDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: true });
|
|
924
|
+
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) });
|
|
919
925
|
}
|
|
920
926
|
|
|
921
927
|
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
|
|
@@ -1338,19 +1344,20 @@ export class DcRouter {
|
|
|
1338
1344
|
/**
|
|
1339
1345
|
* Generate SmartProxy routes for DNS configuration
|
|
1340
1346
|
*/
|
|
1341
|
-
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
|
1347
|
+
private generateDnsRoutes(options?: { includeSocketHandler?: boolean }): plugins.smartproxy.IRouteConfig[] {
|
|
1342
1348
|
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
|
1343
1349
|
return [];
|
|
1344
1350
|
}
|
|
1345
|
-
|
|
1351
|
+
|
|
1352
|
+
const includeSocketHandler = options?.includeSocketHandler !== false;
|
|
1346
1353
|
const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
1347
|
-
|
|
1354
|
+
|
|
1348
1355
|
// Create routes for DNS-over-HTTPS paths
|
|
1349
1356
|
const dohPaths = ['/dns-query', '/resolve'];
|
|
1350
|
-
|
|
1357
|
+
|
|
1351
1358
|
// Use the first nameserver domain for DoH routes
|
|
1352
1359
|
const primaryNameserver = this.options.dnsNsDomains[0];
|
|
1353
|
-
|
|
1360
|
+
|
|
1354
1361
|
for (const path of dohPaths) {
|
|
1355
1362
|
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
|
1356
1363
|
name: `dns-over-https-${path.replace('/', '')}`,
|
|
@@ -1359,18 +1366,42 @@ export class DcRouter {
|
|
|
1359
1366
|
domains: [primaryNameserver],
|
|
1360
1367
|
path: path
|
|
1361
1368
|
},
|
|
1362
|
-
action:
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1369
|
+
action: includeSocketHandler
|
|
1370
|
+
? {
|
|
1371
|
+
type: 'socket-handler' as any,
|
|
1372
|
+
socketHandler: this.createDnsSocketHandler()
|
|
1373
|
+
} as any
|
|
1374
|
+
: {
|
|
1375
|
+
type: 'socket-handler' as any,
|
|
1376
|
+
} as any
|
|
1366
1377
|
};
|
|
1367
|
-
|
|
1378
|
+
|
|
1368
1379
|
dnsRoutes.push(dohRoute);
|
|
1369
1380
|
}
|
|
1370
|
-
|
|
1381
|
+
|
|
1371
1382
|
return dnsRoutes;
|
|
1372
1383
|
}
|
|
1373
1384
|
|
|
1385
|
+
private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
|
|
1386
|
+
const routeName = storedRoute.route.name || '';
|
|
1387
|
+
const isDohRoute = storedRoute.origin === 'dns'
|
|
1388
|
+
&& storedRoute.route.action?.type === 'socket-handler'
|
|
1389
|
+
&& routeName.startsWith('dns-over-https-');
|
|
1390
|
+
|
|
1391
|
+
if (!isDohRoute) {
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return {
|
|
1396
|
+
...storedRoute.route,
|
|
1397
|
+
action: {
|
|
1398
|
+
...storedRoute.route.action,
|
|
1399
|
+
type: 'socket-handler' as any,
|
|
1400
|
+
socketHandler: this.createDnsSocketHandler(),
|
|
1401
|
+
} as any,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1374
1405
|
/**
|
|
1375
1406
|
* Check if a domain matches a pattern (including wildcard support)
|
|
1376
1407
|
* @param domain The domain to check
|
|
@@ -1939,37 +1970,20 @@ export class DcRouter {
|
|
|
1939
1970
|
for (const domainConfig of internalDnsDomains) {
|
|
1940
1971
|
const domain = domainConfig.domain;
|
|
1941
1972
|
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
|
1942
|
-
const
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
type: 'TXT',
|
|
1957
|
-
value: spfRecord,
|
|
1958
|
-
ttl
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
// DMARC record - using sensible defaults
|
|
1962
|
-
const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring
|
|
1963
|
-
const dmarcEmail = `dmarc@${domain}`;
|
|
1964
|
-
records.push({
|
|
1965
|
-
name: `_dmarc.${domain}`,
|
|
1966
|
-
type: 'TXT',
|
|
1967
|
-
value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`,
|
|
1968
|
-
ttl
|
|
1969
|
-
});
|
|
1970
|
-
|
|
1971
|
-
// Note: DKIM records will be generated later when DKIM keys are available
|
|
1972
|
-
// They require the DKIMCreator which is part of the email server
|
|
1973
|
+
const requiredRecords = buildEmailDnsRecords({
|
|
1974
|
+
domain,
|
|
1975
|
+
hostname: this.options.emailConfig.hostname,
|
|
1976
|
+
mxPriority: domainConfig.dns?.internal?.mxPriority,
|
|
1977
|
+
}).filter((record) => !record.name.includes('._domainkey.'));
|
|
1978
|
+
|
|
1979
|
+
for (const record of requiredRecords) {
|
|
1980
|
+
records.push({
|
|
1981
|
+
name: record.name,
|
|
1982
|
+
type: record.type,
|
|
1983
|
+
value: record.value,
|
|
1984
|
+
ttl,
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1973
1987
|
}
|
|
1974
1988
|
|
|
1975
1989
|
logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
|
|
@@ -14,6 +14,11 @@ import type { ReferenceResolver } from './classes.reference-resolver.js';
|
|
|
14
14
|
/** An IP allow entry: plain IP/CIDR or domain-scoped. */
|
|
15
15
|
export type TIpAllowEntry = string | { ip: string; domains: string[] };
|
|
16
16
|
|
|
17
|
+
export interface IRouteMutationResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
|
|
19
24
|
* never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
|
|
@@ -56,6 +61,7 @@ export class RouteConfigManager {
|
|
|
56
61
|
private referenceResolver?: ReferenceResolver,
|
|
57
62
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
|
58
63
|
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
|
|
64
|
+
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
|
|
59
65
|
) {}
|
|
60
66
|
|
|
61
67
|
/** Expose routes map for reference resolution lookups. */
|
|
@@ -63,6 +69,10 @@ export class RouteConfigManager {
|
|
|
63
69
|
return this.routes;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
public getRoute(id: string): IRoute | undefined {
|
|
73
|
+
return this.routes.get(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
/**
|
|
67
77
|
* Load persisted routes, seed serializable config/email/dns routes,
|
|
68
78
|
* compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
|
|
@@ -94,6 +104,7 @@ export class RouteConfigManager {
|
|
|
94
104
|
id: route.id,
|
|
95
105
|
enabled: route.enabled,
|
|
96
106
|
origin: route.origin,
|
|
107
|
+
systemKey: route.systemKey,
|
|
97
108
|
createdAt: route.createdAt,
|
|
98
109
|
updatedAt: route.updatedAt,
|
|
99
110
|
metadata: route.metadata,
|
|
@@ -153,9 +164,21 @@ export class RouteConfigManager {
|
|
|
153
164
|
enabled?: boolean;
|
|
154
165
|
metadata?: Partial<IRouteMetadata>;
|
|
155
166
|
},
|
|
156
|
-
): Promise<
|
|
167
|
+
): Promise<IRouteMutationResult> {
|
|
157
168
|
const stored = this.routes.get(id);
|
|
158
|
-
if (!stored)
|
|
169
|
+
if (!stored) {
|
|
170
|
+
return { success: false, message: 'Route not found' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isToggleOnlyPatch = patch.enabled !== undefined
|
|
174
|
+
&& patch.route === undefined
|
|
175
|
+
&& patch.metadata === undefined;
|
|
176
|
+
if (stored.origin !== 'api' && !isToggleOnlyPatch) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
message: 'System routes are managed by the system and can only be toggled',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
159
182
|
|
|
160
183
|
if (patch.route) {
|
|
161
184
|
const mergedAction = patch.route.action
|
|
@@ -189,19 +212,29 @@ export class RouteConfigManager {
|
|
|
189
212
|
|
|
190
213
|
await this.persistRoute(stored);
|
|
191
214
|
await this.applyRoutes();
|
|
192
|
-
return true;
|
|
215
|
+
return { success: true };
|
|
193
216
|
}
|
|
194
217
|
|
|
195
|
-
public async deleteRoute(id: string): Promise<
|
|
196
|
-
|
|
218
|
+
public async deleteRoute(id: string): Promise<IRouteMutationResult> {
|
|
219
|
+
const stored = this.routes.get(id);
|
|
220
|
+
if (!stored) {
|
|
221
|
+
return { success: false, message: 'Route not found' };
|
|
222
|
+
}
|
|
223
|
+
if (stored.origin !== 'api') {
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
message: 'System routes are managed by the system and cannot be deleted',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
197
230
|
this.routes.delete(id);
|
|
198
231
|
const doc = await RouteDoc.findById(id);
|
|
199
232
|
if (doc) await doc.delete();
|
|
200
233
|
await this.applyRoutes();
|
|
201
|
-
return true;
|
|
234
|
+
return { success: true };
|
|
202
235
|
}
|
|
203
236
|
|
|
204
|
-
public async toggleRoute(id: string, enabled: boolean): Promise<
|
|
237
|
+
public async toggleRoute(id: string, enabled: boolean): Promise<IRouteMutationResult> {
|
|
205
238
|
return this.updateRoute(id, { enabled });
|
|
206
239
|
}
|
|
207
240
|
|
|
@@ -217,29 +250,28 @@ export class RouteConfigManager {
|
|
|
217
250
|
seedRoutes: IDcRouterRouteConfig[],
|
|
218
251
|
origin: 'config' | 'email' | 'dns',
|
|
219
252
|
): Promise<void> {
|
|
220
|
-
|
|
221
|
-
|
|
253
|
+
const seedSystemKeys = new Set<string>();
|
|
222
254
|
const seedNames = new Set<string>();
|
|
223
255
|
let seeded = 0;
|
|
224
256
|
let updated = 0;
|
|
225
257
|
|
|
226
258
|
for (const route of seedRoutes) {
|
|
227
259
|
const name = route.name || '';
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
existingId = id;
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
260
|
+
if (name) {
|
|
261
|
+
seedNames.add(name);
|
|
262
|
+
}
|
|
263
|
+
const systemKey = this.buildSystemRouteKey(origin, route);
|
|
264
|
+
if (systemKey) {
|
|
265
|
+
seedSystemKeys.add(systemKey);
|
|
237
266
|
}
|
|
238
267
|
|
|
268
|
+
const existingId = this.findExistingSeedRouteId(origin, route, systemKey);
|
|
269
|
+
|
|
239
270
|
if (existingId) {
|
|
240
271
|
// Update route config but preserve enabled state
|
|
241
272
|
const existing = this.routes.get(existingId)!;
|
|
242
273
|
existing.route = route;
|
|
274
|
+
existing.systemKey = systemKey;
|
|
243
275
|
existing.updatedAt = Date.now();
|
|
244
276
|
await this.persistRoute(existing);
|
|
245
277
|
updated++;
|
|
@@ -255,6 +287,7 @@ export class RouteConfigManager {
|
|
|
255
287
|
updatedAt: now,
|
|
256
288
|
createdBy: 'system',
|
|
257
289
|
origin,
|
|
290
|
+
systemKey,
|
|
258
291
|
};
|
|
259
292
|
this.routes.set(id, newRoute);
|
|
260
293
|
await this.persistRoute(newRoute);
|
|
@@ -265,7 +298,12 @@ export class RouteConfigManager {
|
|
|
265
298
|
// Delete stale routes: same origin but name not in current seed set
|
|
266
299
|
const staleIds: string[] = [];
|
|
267
300
|
for (const [id, r] of this.routes) {
|
|
268
|
-
if (r.origin
|
|
301
|
+
if (r.origin !== origin) continue;
|
|
302
|
+
|
|
303
|
+
const routeName = r.route.name || '';
|
|
304
|
+
const matchesSeedSystemKey = r.systemKey ? seedSystemKeys.has(r.systemKey) : false;
|
|
305
|
+
const matchesSeedName = routeName ? seedNames.has(routeName) : false;
|
|
306
|
+
if (!matchesSeedSystemKey && !matchesSeedName) {
|
|
269
307
|
staleIds.push(id);
|
|
270
308
|
}
|
|
271
309
|
}
|
|
@@ -284,9 +322,39 @@ export class RouteConfigManager {
|
|
|
284
322
|
// Private: persistence
|
|
285
323
|
// =========================================================================
|
|
286
324
|
|
|
325
|
+
private buildSystemRouteKey(
|
|
326
|
+
origin: 'config' | 'email' | 'dns',
|
|
327
|
+
route: IDcRouterRouteConfig,
|
|
328
|
+
): string | undefined {
|
|
329
|
+
const name = route.name?.trim();
|
|
330
|
+
if (!name) return undefined;
|
|
331
|
+
return `${origin}:${name}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private findExistingSeedRouteId(
|
|
335
|
+
origin: 'config' | 'email' | 'dns',
|
|
336
|
+
route: IDcRouterRouteConfig,
|
|
337
|
+
systemKey?: string,
|
|
338
|
+
): string | undefined {
|
|
339
|
+
const routeName = route.name || '';
|
|
340
|
+
|
|
341
|
+
for (const [id, storedRoute] of this.routes) {
|
|
342
|
+
if (storedRoute.origin !== origin) continue;
|
|
343
|
+
|
|
344
|
+
if (systemKey && storedRoute.systemKey === systemKey) {
|
|
345
|
+
return id;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (storedRoute.route.name === routeName) {
|
|
349
|
+
return id;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
287
356
|
private async loadRoutes(): Promise<void> {
|
|
288
357
|
const docs = await RouteDoc.findAll();
|
|
289
|
-
let prunedRuntimeRoutes = 0;
|
|
290
358
|
|
|
291
359
|
for (const doc of docs) {
|
|
292
360
|
if (!doc.id) continue;
|
|
@@ -299,27 +367,15 @@ export class RouteConfigManager {
|
|
|
299
367
|
updatedAt: doc.updatedAt,
|
|
300
368
|
createdBy: doc.createdBy,
|
|
301
369
|
origin: doc.origin || 'api',
|
|
370
|
+
systemKey: doc.systemKey,
|
|
302
371
|
metadata: doc.metadata,
|
|
303
372
|
};
|
|
304
373
|
|
|
305
|
-
if (this.isPersistedRuntimeRoute(storedRoute)) {
|
|
306
|
-
await doc.delete();
|
|
307
|
-
prunedRuntimeRoutes++;
|
|
308
|
-
logger.log(
|
|
309
|
-
'warn',
|
|
310
|
-
`Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`,
|
|
311
|
-
);
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
374
|
this.routes.set(doc.id, storedRoute);
|
|
316
375
|
}
|
|
317
376
|
if (this.routes.size > 0) {
|
|
318
377
|
logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
|
|
319
378
|
}
|
|
320
|
-
if (prunedRuntimeRoutes > 0) {
|
|
321
|
-
logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`);
|
|
322
|
-
}
|
|
323
379
|
}
|
|
324
380
|
|
|
325
381
|
private async persistRoute(stored: IRoute): Promise<void> {
|
|
@@ -330,6 +386,7 @@ export class RouteConfigManager {
|
|
|
330
386
|
existingDoc.updatedAt = stored.updatedAt;
|
|
331
387
|
existingDoc.createdBy = stored.createdBy;
|
|
332
388
|
existingDoc.origin = stored.origin;
|
|
389
|
+
existingDoc.systemKey = stored.systemKey;
|
|
333
390
|
existingDoc.metadata = stored.metadata;
|
|
334
391
|
await existingDoc.save();
|
|
335
392
|
} else {
|
|
@@ -341,6 +398,7 @@ export class RouteConfigManager {
|
|
|
341
398
|
doc.updatedAt = stored.updatedAt;
|
|
342
399
|
doc.createdBy = stored.createdBy;
|
|
343
400
|
doc.origin = stored.origin;
|
|
401
|
+
doc.systemKey = stored.systemKey;
|
|
344
402
|
doc.metadata = stored.metadata;
|
|
345
403
|
await doc.save();
|
|
346
404
|
}
|
|
@@ -411,7 +469,7 @@ export class RouteConfigManager {
|
|
|
411
469
|
// Add all enabled routes with HTTP/3 and VPN augmentation
|
|
412
470
|
for (const route of this.routes.values()) {
|
|
413
471
|
if (route.enabled) {
|
|
414
|
-
enabledRoutes.push(this.
|
|
472
|
+
enabledRoutes.push(this.prepareStoredRouteForApply(route));
|
|
415
473
|
}
|
|
416
474
|
}
|
|
417
475
|
|
|
@@ -431,6 +489,11 @@ export class RouteConfigManager {
|
|
|
431
489
|
});
|
|
432
490
|
}
|
|
433
491
|
|
|
492
|
+
private prepareStoredRouteForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig {
|
|
493
|
+
const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
|
|
494
|
+
return this.prepareRouteForApply(hydratedRoute || storedRoute.route, storedRoute.id);
|
|
495
|
+
}
|
|
496
|
+
|
|
434
497
|
private prepareRouteForApply(
|
|
435
498
|
route: plugins.smartproxy.IRouteConfig,
|
|
436
499
|
routeId?: string,
|
|
@@ -465,12 +528,4 @@ export class RouteConfigManager {
|
|
|
465
528
|
},
|
|
466
529
|
};
|
|
467
530
|
}
|
|
468
|
-
|
|
469
|
-
private isPersistedRuntimeRoute(storedRoute: IRoute): boolean {
|
|
470
|
-
const routeName = storedRoute.route.name || '';
|
|
471
|
-
const actionType = storedRoute.route.action?.type;
|
|
472
|
-
|
|
473
|
-
return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler')
|
|
474
|
-
|| (storedRoute.origin === 'dns' && actionType === 'socket-handler');
|
|
475
|
-
}
|
|
476
531
|
}
|
|
@@ -29,6 +29,9 @@ export class RouteDoc extends plugins.smartdata.SmartDataDbDoc<RouteDoc, RouteDo
|
|
|
29
29
|
@plugins.smartdata.svDb()
|
|
30
30
|
public origin!: 'config' | 'email' | 'dns' | 'api';
|
|
31
31
|
|
|
32
|
+
@plugins.smartdata.svDb()
|
|
33
|
+
public systemKey?: string;
|
|
34
|
+
|
|
32
35
|
@plugins.smartdata.svDb()
|
|
33
36
|
public metadata?: IRouteMetadata;
|
|
34
37
|
|
|
@@ -51,4 +54,8 @@ export class RouteDoc extends plugins.smartdata.SmartDataDbDoc<RouteDoc, RouteDo
|
|
|
51
54
|
public static async findByOrigin(origin: 'config' | 'email' | 'dns' | 'api'): Promise<RouteDoc[]> {
|
|
52
55
|
return await RouteDoc.getInstances({ origin });
|
|
53
56
|
}
|
|
57
|
+
|
|
58
|
+
public static async findBySystemKey(systemKey: string): Promise<RouteDoc | null> {
|
|
59
|
+
return await RouteDoc.getInstance({ systemKey });
|
|
60
|
+
}
|
|
54
61
|
}
|
|
@@ -6,6 +6,7 @@ import { DomainDoc } from '../db/documents/classes.domain.doc.js';
|
|
|
6
6
|
import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
|
|
7
7
|
import type { DnsManager } from '../dns/manager.dns.js';
|
|
8
8
|
import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js';
|
|
9
|
+
import { buildEmailDnsRecords } from './email-dns-records.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* EmailDomainManager — orchestrates email domain setup.
|
|
@@ -181,34 +182,13 @@ export class EmailDomainManager {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
type: 'TXT',
|
|
193
|
-
name: domain,
|
|
194
|
-
value: 'v=spf1 a mx ~all',
|
|
195
|
-
status: doc.dnsStatus.spf,
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
type: 'TXT',
|
|
199
|
-
name: `${selector}._domainkey.${domain}`,
|
|
200
|
-
value: dkimValue,
|
|
201
|
-
status: doc.dnsStatus.dkim,
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
type: 'TXT',
|
|
205
|
-
name: `_dmarc.${domain}`,
|
|
206
|
-
value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`,
|
|
207
|
-
status: doc.dnsStatus.dmarc,
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
return records;
|
|
185
|
+
return buildEmailDnsRecords({
|
|
186
|
+
domain,
|
|
187
|
+
hostname,
|
|
188
|
+
selector,
|
|
189
|
+
dkimValue,
|
|
190
|
+
statuses: doc.dnsStatus,
|
|
191
|
+
});
|
|
212
192
|
}
|
|
213
193
|
|
|
214
194
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
@@ -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
|
);
|