@serve.zone/dcrouter 12.10.0 → 13.0.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.
Files changed (86) hide show
  1. package/dist_serve/bundle.js +957 -866
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/classes.dcrouter.d.ts +5 -4
  4. package/dist_ts/classes.dcrouter.js +25 -49
  5. package/dist_ts/config/classes.reference-resolver.d.ts +7 -7
  6. package/dist_ts/config/classes.reference-resolver.js +27 -27
  7. package/dist_ts/config/classes.route-config-manager.d.ts +2 -2
  8. package/dist_ts/config/classes.route-config-manager.js +12 -15
  9. package/dist_ts/config/classes.target-profile-manager.d.ts +63 -0
  10. package/dist_ts/config/classes.target-profile-manager.js +295 -0
  11. package/dist_ts/config/index.d.ts +1 -0
  12. package/dist_ts/config/index.js +2 -1
  13. package/dist_ts/db/documents/{classes.security-profile.doc.d.ts → classes.source-profile.doc.d.ts} +4 -4
  14. package/dist_ts/db/documents/{classes.security-profile.doc.js → classes.source-profile.doc.js} +9 -9
  15. package/dist_ts/db/documents/classes.target-profile.doc.d.ts +17 -0
  16. package/dist_ts/db/documents/classes.target-profile.doc.js +124 -0
  17. package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +1 -1
  18. package/dist_ts/db/documents/classes.vpn-client.doc.js +8 -8
  19. package/dist_ts/db/documents/index.d.ts +2 -1
  20. package/dist_ts/db/documents/index.js +3 -2
  21. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -1
  22. package/dist_ts/opsserver/classes.opsserver.js +5 -3
  23. package/dist_ts/opsserver/handlers/index.d.ts +2 -1
  24. package/dist_ts/opsserver/handlers/index.js +3 -2
  25. package/dist_ts/opsserver/handlers/{security-profile.handler.d.ts → source-profile.handler.d.ts} +1 -1
  26. package/dist_ts/opsserver/handlers/{security-profile.handler.js → source-profile.handler.js} +20 -20
  27. package/dist_ts/opsserver/handlers/target-profile.handler.d.ts +10 -0
  28. package/dist_ts/opsserver/handlers/target-profile.handler.js +115 -0
  29. package/dist_ts/opsserver/handlers/vpn.handler.js +5 -5
  30. package/dist_ts/vpn/classes.vpn-manager.d.ts +6 -10
  31. package/dist_ts/vpn/classes.vpn-manager.js +11 -34
  32. package/dist_ts_interfaces/data/index.d.ts +1 -0
  33. package/dist_ts_interfaces/data/index.js +2 -1
  34. package/dist_ts_interfaces/data/remoteingress.d.ts +4 -15
  35. package/dist_ts_interfaces/data/route-management.d.ts +9 -6
  36. package/dist_ts_interfaces/data/target-profile.d.ts +28 -0
  37. package/dist_ts_interfaces/data/target-profile.js +2 -0
  38. package/dist_ts_interfaces/data/vpn.d.ts +2 -1
  39. package/dist_ts_interfaces/requests/index.d.ts +2 -1
  40. package/dist_ts_interfaces/requests/index.js +3 -2
  41. package/dist_ts_interfaces/requests/{security-profiles.d.ts → source-profiles.d.ts} +21 -21
  42. package/dist_ts_interfaces/requests/source-profiles.js +2 -0
  43. package/dist_ts_interfaces/requests/target-profiles.d.ts +103 -0
  44. package/dist_ts_interfaces/requests/target-profiles.js +2 -0
  45. package/dist_ts_interfaces/requests/vpn.d.ts +2 -2
  46. package/dist_ts_web/00_commitinfo_data.js +2 -2
  47. package/dist_ts_web/appstate.d.ts +36 -3
  48. package/dist_ts_web/appstate.js +127 -10
  49. package/dist_ts_web/elements/index.d.ts +2 -1
  50. package/dist_ts_web/elements/index.js +3 -2
  51. package/dist_ts_web/elements/ops-dashboard.js +10 -4
  52. package/dist_ts_web/elements/ops-view-routes.js +8 -8
  53. package/dist_ts_web/elements/{ops-view-securityprofiles.d.ts → ops-view-sourceprofiles.d.ts} +2 -2
  54. package/dist_ts_web/elements/{ops-view-securityprofiles.js → ops-view-sourceprofiles.js} +12 -12
  55. package/dist_ts_web/elements/ops-view-targetprofiles.d.ts +19 -0
  56. package/dist_ts_web/elements/ops-view-targetprofiles.js +412 -0
  57. package/dist_ts_web/elements/ops-view-vpn.js +13 -13
  58. package/dist_ts_web/router.d.ts +1 -1
  59. package/dist_ts_web/router.js +2 -2
  60. package/package.json +1 -1
  61. package/ts/00_commitinfo_data.ts +1 -1
  62. package/ts/classes.dcrouter.ts +33 -50
  63. package/ts/config/classes.reference-resolver.ts +34 -34
  64. package/ts/config/classes.route-config-manager.ts +9 -12
  65. package/ts/config/classes.target-profile-manager.ts +348 -0
  66. package/ts/config/index.ts +2 -1
  67. package/ts/db/documents/{classes.security-profile.doc.ts → classes.source-profile.doc.ts} +7 -7
  68. package/ts/db/documents/classes.target-profile.doc.ts +52 -0
  69. package/ts/db/documents/classes.vpn-client.doc.ts +1 -1
  70. package/ts/db/documents/index.ts +2 -1
  71. package/ts/opsserver/classes.opsserver.ts +4 -2
  72. package/ts/opsserver/handlers/index.ts +2 -1
  73. package/ts/opsserver/handlers/{security-profile.handler.ts → source-profile.handler.ts} +25 -25
  74. package/ts/opsserver/handlers/target-profile.handler.ts +155 -0
  75. package/ts/opsserver/handlers/vpn.handler.ts +4 -4
  76. package/ts/vpn/classes.vpn-manager.ts +14 -38
  77. package/ts_web/00_commitinfo_data.ts +1 -1
  78. package/ts_web/appstate.ts +180 -17
  79. package/ts_web/elements/index.ts +2 -1
  80. package/ts_web/elements/ops-dashboard.ts +9 -3
  81. package/ts_web/elements/ops-view-routes.ts +7 -7
  82. package/ts_web/elements/{ops-view-securityprofiles.ts → ops-view-sourceprofiles.ts} +13 -13
  83. package/ts_web/elements/ops-view-targetprofiles.ts +379 -0
  84. package/ts_web/elements/ops-view-vpn.ts +12 -12
  85. package/ts_web/router.ts +1 -1
  86. package/dist_ts_interfaces/requests/security-profiles.js +0 -2
@@ -1,8 +1,8 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
- import { SecurityProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
3
+ import { SourceProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
4
4
  import type {
5
- ISecurityProfile,
5
+ ISourceProfile,
6
6
  INetworkTarget,
7
7
  IRouteMetadata,
8
8
  IStoredRoute,
@@ -12,7 +12,7 @@ import type {
12
12
  const MAX_INHERITANCE_DEPTH = 5;
13
13
 
14
14
  export class ReferenceResolver {
15
- private profiles = new Map<string, ISecurityProfile>();
15
+ private profiles = new Map<string, ISourceProfile>();
16
16
  private targets = new Map<string, INetworkTarget>();
17
17
 
18
18
  // =========================================================================
@@ -38,7 +38,7 @@ export class ReferenceResolver {
38
38
  const id = plugins.uuid.v4();
39
39
  const now = Date.now();
40
40
 
41
- const profile: ISecurityProfile = {
41
+ const profile: ISourceProfile = {
42
42
  id,
43
43
  name: data.name,
44
44
  description: data.description,
@@ -51,17 +51,17 @@ export class ReferenceResolver {
51
51
 
52
52
  this.profiles.set(id, profile);
53
53
  await this.persistProfile(profile);
54
- logger.log('info', `Created security profile '${profile.name}' (${id})`);
54
+ logger.log('info', `Created source profile '${profile.name}' (${id})`);
55
55
  return id;
56
56
  }
57
57
 
58
58
  public async updateProfile(
59
59
  id: string,
60
- patch: Partial<Omit<ISecurityProfile, 'id' | 'createdAt' | 'createdBy'>>,
60
+ patch: Partial<Omit<ISourceProfile, 'id' | 'createdAt' | 'createdBy'>>,
61
61
  ): Promise<{ affectedRouteIds: string[] }> {
62
62
  const profile = this.profiles.get(id);
63
63
  if (!profile) {
64
- throw new Error(`Security profile '${id}' not found`);
64
+ throw new Error(`Source profile '${id}' not found`);
65
65
  }
66
66
 
67
67
  if (patch.name !== undefined) profile.name = patch.name;
@@ -71,7 +71,7 @@ export class ReferenceResolver {
71
71
  profile.updatedAt = Date.now();
72
72
 
73
73
  await this.persistProfile(profile);
74
- logger.log('info', `Updated security profile '${profile.name}' (${id})`);
74
+ logger.log('info', `Updated source profile '${profile.name}' (${id})`);
75
75
 
76
76
  // Find routes referencing this profile
77
77
  const affectedRouteIds = await this.findRoutesByProfileRef(id);
@@ -85,7 +85,7 @@ export class ReferenceResolver {
85
85
  ): Promise<{ success: boolean; message?: string }> {
86
86
  const profile = this.profiles.get(id);
87
87
  if (!profile) {
88
- return { success: false, message: `Security profile '${id}' not found` };
88
+ return { success: false, message: `Source profile '${id}' not found` };
89
89
  }
90
90
 
91
91
  // Check usage
@@ -101,7 +101,7 @@ export class ReferenceResolver {
101
101
  }
102
102
 
103
103
  // Delete from DB
104
- const doc = await SecurityProfileDoc.findById(id);
104
+ const doc = await SourceProfileDoc.findById(id);
105
105
  if (doc) await doc.delete();
106
106
  this.profiles.delete(id);
107
107
 
@@ -110,24 +110,24 @@ export class ReferenceResolver {
110
110
  await this.clearProfileRefsOnRoutes(affectedIds);
111
111
  logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
112
112
  } else {
113
- logger.log('info', `Deleted security profile '${profile.name}' (${id})`);
113
+ logger.log('info', `Deleted source profile '${profile.name}' (${id})`);
114
114
  }
115
115
 
116
116
  return { success: true };
117
117
  }
118
118
 
119
- public getProfile(id: string): ISecurityProfile | undefined {
119
+ public getProfile(id: string): ISourceProfile | undefined {
120
120
  return this.profiles.get(id);
121
121
  }
122
122
 
123
- public getProfileByName(name: string): ISecurityProfile | undefined {
123
+ public getProfileByName(name: string): ISourceProfile | undefined {
124
124
  for (const profile of this.profiles.values()) {
125
125
  if (profile.name === name) return profile;
126
126
  }
127
127
  return undefined;
128
128
  }
129
129
 
130
- public listProfiles(): ISecurityProfile[] {
130
+ public listProfiles(): ISourceProfile[] {
131
131
  return [...this.profiles.values()];
132
132
  }
133
133
 
@@ -137,7 +137,7 @@ export class ReferenceResolver {
137
137
  usage.set(profile.id, []);
138
138
  }
139
139
  for (const [routeId, stored] of storedRoutes) {
140
- const ref = stored.metadata?.securityProfileRef;
140
+ const ref = stored.metadata?.sourceProfileRef;
141
141
  if (ref && usage.has(ref)) {
142
142
  usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
143
143
  }
@@ -151,7 +151,7 @@ export class ReferenceResolver {
151
151
  ): Array<{ id: string; routeName: string }> {
152
152
  const routes: Array<{ id: string; routeName: string }> = [];
153
153
  for (const [routeId, stored] of storedRoutes) {
154
- if (stored.metadata?.securityProfileRef === profileId) {
154
+ if (stored.metadata?.sourceProfileRef === profileId) {
155
155
  routes.push({ id: routeId, routeName: stored.route.name || routeId });
156
156
  }
157
157
  }
@@ -280,7 +280,7 @@ export class ReferenceResolver {
280
280
 
281
281
  /**
282
282
  * Resolve references for a single route.
283
- * Materializes security profile and/or network target into the route's fields.
283
+ * Materializes source profile and/or network target into the route's fields.
284
284
  * Returns the resolved route and updated metadata.
285
285
  */
286
286
  public resolveRoute(
@@ -289,19 +289,19 @@ export class ReferenceResolver {
289
289
  ): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
290
290
  const resolvedMetadata: IRouteMetadata = { ...metadata };
291
291
 
292
- if (resolvedMetadata.securityProfileRef) {
293
- const resolvedSecurity = this.resolveSecurityProfile(resolvedMetadata.securityProfileRef);
292
+ if (resolvedMetadata.sourceProfileRef) {
293
+ const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
294
294
  if (resolvedSecurity) {
295
- const profile = this.profiles.get(resolvedMetadata.securityProfileRef);
295
+ const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
296
296
  // Merge: profile provides base, route's inline values override
297
297
  route = {
298
298
  ...route,
299
299
  security: this.mergeSecurityFields(resolvedSecurity, route.security),
300
300
  };
301
- resolvedMetadata.securityProfileName = profile?.name;
301
+ resolvedMetadata.sourceProfileName = profile?.name;
302
302
  resolvedMetadata.lastResolvedAt = Date.now();
303
303
  } else {
304
- logger.log('warn', `Security profile '${resolvedMetadata.securityProfileRef}' not found during resolution`);
304
+ logger.log('warn', `Source profile '${resolvedMetadata.sourceProfileRef}' not found during resolution`);
305
305
  }
306
306
  }
307
307
 
@@ -335,7 +335,7 @@ export class ReferenceResolver {
335
335
  public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
336
336
  const docs = await StoredRouteDoc.findAll();
337
337
  return docs
338
- .filter((doc) => doc.metadata?.securityProfileRef === profileId)
338
+ .filter((doc) => doc.metadata?.sourceProfileRef === profileId)
339
339
  .map((doc) => doc.id);
340
340
  }
341
341
 
@@ -349,7 +349,7 @@ export class ReferenceResolver {
349
349
  public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
350
350
  const ids: string[] = [];
351
351
  for (const [routeId, stored] of storedRoutes) {
352
- if (stored.metadata?.securityProfileRef === profileId) {
352
+ if (stored.metadata?.sourceProfileRef === profileId) {
353
353
  ids.push(routeId);
354
354
  }
355
355
  }
@@ -367,10 +367,10 @@ export class ReferenceResolver {
367
367
  }
368
368
 
369
369
  // =========================================================================
370
- // Private: security profile resolution with inheritance
370
+ // Private: source profile resolution with inheritance
371
371
  // =========================================================================
372
372
 
373
- private resolveSecurityProfile(
373
+ private resolveSourceProfile(
374
374
  profileId: string,
375
375
  visited: Set<string> = new Set(),
376
376
  depth: number = 0,
@@ -396,7 +396,7 @@ export class ReferenceResolver {
396
396
  // Resolve parent profiles first (top-down, later overrides earlier)
397
397
  if (profile.extendsProfiles?.length) {
398
398
  for (const parentId of profile.extendsProfiles) {
399
- const parentSecurity = this.resolveSecurityProfile(parentId, new Set(visited), depth + 1);
399
+ const parentSecurity = this.resolveSourceProfile(parentId, new Set(visited), depth + 1);
400
400
  if (parentSecurity) {
401
401
  baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
402
402
  }
@@ -453,7 +453,7 @@ export class ReferenceResolver {
453
453
  // =========================================================================
454
454
 
455
455
  private async loadProfiles(): Promise<void> {
456
- const docs = await SecurityProfileDoc.findAll();
456
+ const docs = await SourceProfileDoc.findAll();
457
457
  for (const doc of docs) {
458
458
  if (doc.id) {
459
459
  this.profiles.set(doc.id, {
@@ -469,7 +469,7 @@ export class ReferenceResolver {
469
469
  }
470
470
  }
471
471
  if (this.profiles.size > 0) {
472
- logger.log('info', `Loaded ${this.profiles.size} security profile(s) from storage`);
472
+ logger.log('info', `Loaded ${this.profiles.size} source profile(s) from storage`);
473
473
  }
474
474
  }
475
475
 
@@ -494,8 +494,8 @@ export class ReferenceResolver {
494
494
  }
495
495
  }
496
496
 
497
- private async persistProfile(profile: ISecurityProfile): Promise<void> {
498
- const existingDoc = await SecurityProfileDoc.findById(profile.id);
497
+ private async persistProfile(profile: ISourceProfile): Promise<void> {
498
+ const existingDoc = await SourceProfileDoc.findById(profile.id);
499
499
  if (existingDoc) {
500
500
  existingDoc.name = profile.name;
501
501
  existingDoc.description = profile.description;
@@ -504,7 +504,7 @@ export class ReferenceResolver {
504
504
  existingDoc.updatedAt = profile.updatedAt;
505
505
  await existingDoc.save();
506
506
  } else {
507
- const doc = new SecurityProfileDoc();
507
+ const doc = new SourceProfileDoc();
508
508
  doc.id = profile.id;
509
509
  doc.name = profile.name;
510
510
  doc.description = profile.description;
@@ -550,8 +550,8 @@ export class ReferenceResolver {
550
550
  if (doc?.metadata) {
551
551
  doc.metadata = {
552
552
  ...doc.metadata,
553
- securityProfileRef: undefined,
554
- securityProfileName: undefined,
553
+ sourceProfileRef: undefined,
554
+ sourceProfileName: undefined,
555
555
  };
556
556
  doc.updatedAt = Date.now();
557
557
  await doc.save();
@@ -21,7 +21,7 @@ export class RouteConfigManager {
21
21
  private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
22
22
  private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
23
23
  private getHttp3Config?: () => IHttp3Config | undefined,
24
- private getVpnAllowList?: (tags?: string[]) => string[],
24
+ private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => string[],
25
25
  private referenceResolver?: ReferenceResolver,
26
26
  private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
27
27
  ) {}
@@ -363,22 +363,19 @@ export class RouteConfigManager {
363
363
  const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
364
364
 
365
365
  const http3Config = this.getHttp3Config?.();
366
- const vpnAllowList = this.getVpnAllowList;
366
+ const vpnCallback = this.getVpnClientIpsForRoute;
367
367
 
368
- // Helper: inject VPN security into a route if vpn.enabled is set
369
- const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => {
370
- if (!vpnAllowList) return route;
368
+ // Helper: inject VPN security into a vpnOnly route
369
+ const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
370
+ if (!vpnCallback) return route;
371
371
  const dcRoute = route as IDcRouterRouteConfig;
372
- if (!dcRoute.vpn?.enabled) return route;
373
- const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
374
- const mandatory = dcRoute.vpn.mandatory === true; // defaults to false
372
+ if (!dcRoute.vpnOnly) return route;
373
+ const allowList = vpnCallback(dcRoute, routeId);
375
374
  return {
376
375
  ...route,
377
376
  security: {
378
377
  ...route.security,
379
- ipAllowList: mandatory
380
- ? allowList
381
- : [...(route.security?.ipAllowList || []), ...allowList],
378
+ ipAllowList: allowList,
382
379
  },
383
380
  };
384
381
  };
@@ -400,7 +397,7 @@ export class RouteConfigManager {
400
397
  if (http3Config?.enabled !== false) {
401
398
  route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
402
399
  }
403
- enabledRoutes.push(injectVpn(route));
400
+ enabledRoutes.push(injectVpn(route, stored.id));
404
401
  }
405
402
  }
406
403
 
@@ -0,0 +1,348 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { TargetProfileDoc, VpnClientDoc } from '../db/index.js';
4
+ import type { ITargetProfile, ITargetProfileTarget } from '../../ts_interfaces/data/target-profile.js';
5
+ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
6
+ import type { IStoredRoute } from '../../ts_interfaces/data/route-management.js';
7
+
8
+ /**
9
+ * Manages TargetProfiles (target-side: what can be accessed).
10
+ * TargetProfiles define what resources a VPN client can reach:
11
+ * domains, specific IP:port targets, and/or direct route references.
12
+ */
13
+ export class TargetProfileManager {
14
+ private profiles = new Map<string, ITargetProfile>();
15
+
16
+ // =========================================================================
17
+ // Lifecycle
18
+ // =========================================================================
19
+
20
+ public async initialize(): Promise<void> {
21
+ await this.loadProfiles();
22
+ }
23
+
24
+ // =========================================================================
25
+ // CRUD
26
+ // =========================================================================
27
+
28
+ public async createProfile(data: {
29
+ name: string;
30
+ description?: string;
31
+ domains?: string[];
32
+ targets?: ITargetProfileTarget[];
33
+ routeRefs?: string[];
34
+ createdBy: string;
35
+ }): Promise<string> {
36
+ const id = plugins.uuid.v4();
37
+ const now = Date.now();
38
+
39
+ const profile: ITargetProfile = {
40
+ id,
41
+ name: data.name,
42
+ description: data.description,
43
+ domains: data.domains,
44
+ targets: data.targets,
45
+ routeRefs: data.routeRefs,
46
+ createdAt: now,
47
+ updatedAt: now,
48
+ createdBy: data.createdBy,
49
+ };
50
+
51
+ this.profiles.set(id, profile);
52
+ await this.persistProfile(profile);
53
+ logger.log('info', `Created target profile '${profile.name}' (${id})`);
54
+ return id;
55
+ }
56
+
57
+ public async updateProfile(
58
+ id: string,
59
+ patch: Partial<Omit<ITargetProfile, 'id' | 'createdAt' | 'createdBy'>>,
60
+ ): Promise<void> {
61
+ const profile = this.profiles.get(id);
62
+ if (!profile) {
63
+ throw new Error(`Target profile '${id}' not found`);
64
+ }
65
+
66
+ if (patch.name !== undefined) profile.name = patch.name;
67
+ if (patch.description !== undefined) profile.description = patch.description;
68
+ if (patch.domains !== undefined) profile.domains = patch.domains;
69
+ if (patch.targets !== undefined) profile.targets = patch.targets;
70
+ if (patch.routeRefs !== undefined) profile.routeRefs = patch.routeRefs;
71
+ profile.updatedAt = Date.now();
72
+
73
+ await this.persistProfile(profile);
74
+ logger.log('info', `Updated target profile '${profile.name}' (${id})`);
75
+ }
76
+
77
+ public async deleteProfile(
78
+ id: string,
79
+ force?: boolean,
80
+ ): Promise<{ success: boolean; message?: string }> {
81
+ const profile = this.profiles.get(id);
82
+ if (!profile) {
83
+ return { success: false, message: `Target profile '${id}' not found` };
84
+ }
85
+
86
+ // Check if any VPN clients reference this profile
87
+ const clients = await VpnClientDoc.findAll();
88
+ const referencingClients = clients.filter(
89
+ (c) => c.targetProfileIds?.includes(id),
90
+ );
91
+
92
+ if (referencingClients.length > 0 && !force) {
93
+ return {
94
+ success: false,
95
+ message: `Profile '${profile.name}' is in use by ${referencingClients.length} VPN client(s). Use force=true to delete.`,
96
+ };
97
+ }
98
+
99
+ // Delete from DB
100
+ const doc = await TargetProfileDoc.findById(id);
101
+ if (doc) await doc.delete();
102
+ this.profiles.delete(id);
103
+
104
+ if (referencingClients.length > 0) {
105
+ // Remove profile ref from clients
106
+ for (const client of referencingClients) {
107
+ client.targetProfileIds = client.targetProfileIds?.filter((pid) => pid !== id);
108
+ client.updatedAt = Date.now();
109
+ await client.save();
110
+ }
111
+ logger.log('warn', `Force-deleted target profile '${profile.name}'; removed refs from ${referencingClients.length} client(s)`);
112
+ } else {
113
+ logger.log('info', `Deleted target profile '${profile.name}' (${id})`);
114
+ }
115
+
116
+ return { success: true };
117
+ }
118
+
119
+ public getProfile(id: string): ITargetProfile | undefined {
120
+ return this.profiles.get(id);
121
+ }
122
+
123
+ public listProfiles(): ITargetProfile[] {
124
+ return [...this.profiles.values()];
125
+ }
126
+
127
+ /**
128
+ * Get which VPN clients reference a target profile.
129
+ */
130
+ public async getProfileUsage(profileId: string): Promise<Array<{ clientId: string; description?: string }>> {
131
+ const clients = await VpnClientDoc.findAll();
132
+ return clients
133
+ .filter((c) => c.targetProfileIds?.includes(profileId))
134
+ .map((c) => ({ clientId: c.clientId, description: c.description }));
135
+ }
136
+
137
+ // =========================================================================
138
+ // Core matching: route → client IPs
139
+ // =========================================================================
140
+
141
+ /**
142
+ * For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
143
+ * matches the route. Returns their assigned IPs for injection into ipAllowList.
144
+ */
145
+ public getMatchingClientIps(
146
+ route: IDcRouterRouteConfig,
147
+ routeId: string | undefined,
148
+ clients: VpnClientDoc[],
149
+ ): string[] {
150
+ const ips: string[] = [];
151
+
152
+ for (const client of clients) {
153
+ if (!client.enabled || !client.assignedIp) continue;
154
+ if (!client.targetProfileIds?.length) continue;
155
+
156
+ // Check if any of the client's profiles match this route
157
+ const matches = client.targetProfileIds.some((profileId) => {
158
+ const profile = this.profiles.get(profileId);
159
+ if (!profile) return false;
160
+ return this.routeMatchesProfile(route, routeId, profile);
161
+ });
162
+
163
+ if (matches) {
164
+ ips.push(client.assignedIp);
165
+ }
166
+ }
167
+
168
+ return ips;
169
+ }
170
+
171
+ /**
172
+ * For a given client (by its targetProfileIds), compute the set of
173
+ * domains and target IPs it can access. Used for WireGuard AllowedIPs.
174
+ */
175
+ public getClientAccessSpec(
176
+ targetProfileIds: string[],
177
+ allRoutes: IDcRouterRouteConfig[],
178
+ storedRoutes: Map<string, IStoredRoute>,
179
+ ): { domains: string[]; targetIps: string[] } {
180
+ const domains = new Set<string>();
181
+ const targetIps = new Set<string>();
182
+
183
+ // Collect all access specifiers from assigned profiles
184
+ for (const profileId of targetProfileIds) {
185
+ const profile = this.profiles.get(profileId);
186
+ if (!profile) continue;
187
+
188
+ // Direct domain entries
189
+ if (profile.domains?.length) {
190
+ for (const d of profile.domains) {
191
+ domains.add(d);
192
+ }
193
+ }
194
+
195
+ // Direct target IP entries
196
+ if (profile.targets?.length) {
197
+ for (const t of profile.targets) {
198
+ targetIps.add(t.host);
199
+ }
200
+ }
201
+
202
+ // Route references: scan constructor routes
203
+ for (const route of allRoutes) {
204
+ if (this.routeMatchesProfile(route as IDcRouterRouteConfig, undefined, profile)) {
205
+ const routeDomains = (route.match as any)?.domains;
206
+ if (Array.isArray(routeDomains)) {
207
+ for (const d of routeDomains) {
208
+ domains.add(d);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // Route references: scan stored routes
215
+ for (const [storedId, stored] of storedRoutes) {
216
+ if (!stored.enabled) continue;
217
+ if (this.routeMatchesProfile(stored.route as IDcRouterRouteConfig, storedId, profile)) {
218
+ const routeDomains = (stored.route.match as any)?.domains;
219
+ if (Array.isArray(routeDomains)) {
220
+ for (const d of routeDomains) {
221
+ domains.add(d);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ return {
229
+ domains: [...domains],
230
+ targetIps: [...targetIps],
231
+ };
232
+ }
233
+
234
+ // =========================================================================
235
+ // Private: matching logic
236
+ // =========================================================================
237
+
238
+ /**
239
+ * Check if a route matches a profile. A profile matches if ANY condition is true:
240
+ * 1. Profile's routeRefs contains the route's name or stored route id
241
+ * 2. Profile's domains overlaps with route.match.domains (wildcard matching)
242
+ * 3. Profile's targets overlaps with route.action.targets (host + port match)
243
+ */
244
+ private routeMatchesProfile(
245
+ route: IDcRouterRouteConfig,
246
+ routeId: string | undefined,
247
+ profile: ITargetProfile,
248
+ ): boolean {
249
+ // 1. Route reference match
250
+ if (profile.routeRefs?.length) {
251
+ if (routeId && profile.routeRefs.includes(routeId)) return true;
252
+ if (route.name && profile.routeRefs.includes(route.name)) return true;
253
+ }
254
+
255
+ // 2. Domain match
256
+ if (profile.domains?.length) {
257
+ const routeDomains: string[] = (route.match as any)?.domains || [];
258
+ for (const profileDomain of profile.domains) {
259
+ for (const routeDomain of routeDomains) {
260
+ if (this.domainMatchesPattern(routeDomain, profileDomain)) return true;
261
+ }
262
+ }
263
+ }
264
+
265
+ // 3. Target match (host + port)
266
+ if (profile.targets?.length) {
267
+ const routeTargets = (route.action as any)?.targets;
268
+ if (Array.isArray(routeTargets)) {
269
+ for (const profileTarget of profile.targets) {
270
+ for (const routeTarget of routeTargets) {
271
+ const routeHost = routeTarget.host;
272
+ const routePort = routeTarget.port;
273
+ if (routeHost === profileTarget.host && routePort === profileTarget.port) {
274
+ return true;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ return false;
282
+ }
283
+
284
+ /**
285
+ * Check if a domain matches a pattern.
286
+ * - '*.example.com' matches 'sub.example.com', 'a.b.example.com'
287
+ * - 'example.com' matches only 'example.com'
288
+ */
289
+ private domainMatchesPattern(domain: string, pattern: string): boolean {
290
+ if (pattern === domain) return true;
291
+ if (pattern.startsWith('*.')) {
292
+ const suffix = pattern.slice(1); // '.example.com'
293
+ return domain.endsWith(suffix) && domain.length > suffix.length;
294
+ }
295
+ return false;
296
+ }
297
+
298
+ // =========================================================================
299
+ // Private: persistence
300
+ // =========================================================================
301
+
302
+ private async loadProfiles(): Promise<void> {
303
+ const docs = await TargetProfileDoc.findAll();
304
+ for (const doc of docs) {
305
+ if (doc.id) {
306
+ this.profiles.set(doc.id, {
307
+ id: doc.id,
308
+ name: doc.name,
309
+ description: doc.description,
310
+ domains: doc.domains,
311
+ targets: doc.targets,
312
+ routeRefs: doc.routeRefs,
313
+ createdAt: doc.createdAt,
314
+ updatedAt: doc.updatedAt,
315
+ createdBy: doc.createdBy,
316
+ });
317
+ }
318
+ }
319
+ if (this.profiles.size > 0) {
320
+ logger.log('info', `Loaded ${this.profiles.size} target profile(s) from storage`);
321
+ }
322
+ }
323
+
324
+ private async persistProfile(profile: ITargetProfile): Promise<void> {
325
+ const existingDoc = await TargetProfileDoc.findById(profile.id);
326
+ if (existingDoc) {
327
+ existingDoc.name = profile.name;
328
+ existingDoc.description = profile.description;
329
+ existingDoc.domains = profile.domains;
330
+ existingDoc.targets = profile.targets;
331
+ existingDoc.routeRefs = profile.routeRefs;
332
+ existingDoc.updatedAt = profile.updatedAt;
333
+ await existingDoc.save();
334
+ } else {
335
+ const doc = new TargetProfileDoc();
336
+ doc.id = profile.id;
337
+ doc.name = profile.name;
338
+ doc.description = profile.description;
339
+ doc.domains = profile.domains;
340
+ doc.targets = profile.targets;
341
+ doc.routeRefs = profile.routeRefs;
342
+ doc.createdAt = profile.createdAt;
343
+ doc.updatedAt = profile.updatedAt;
344
+ doc.createdBy = profile.createdBy;
345
+ await doc.save();
346
+ }
347
+ }
348
+ }
@@ -3,4 +3,5 @@ export * from './validator.js';
3
3
  export { RouteConfigManager } from './classes.route-config-manager.js';
4
4
  export { ApiTokenManager } from './classes.api-token-manager.js';
5
5
  export { ReferenceResolver } from './classes.reference-resolver.js';
6
- export { DbSeeder } from './classes.db-seeder.js';
6
+ export { DbSeeder } from './classes.db-seeder.js';
7
+ export { TargetProfileManager } from './classes.target-profile-manager.js';
@@ -5,7 +5,7 @@ import type { IRouteSecurity } from '../../../ts_interfaces/data/route-managemen
5
5
  const getDb = () => DcRouterDb.getInstance().getDb();
6
6
 
7
7
  @plugins.smartdata.Collection(() => getDb())
8
- export class SecurityProfileDoc extends plugins.smartdata.SmartDataDbDoc<SecurityProfileDoc, SecurityProfileDoc> {
8
+ export class SourceProfileDoc extends plugins.smartdata.SmartDataDbDoc<SourceProfileDoc, SourceProfileDoc> {
9
9
  @plugins.smartdata.unI()
10
10
  @plugins.smartdata.svDb()
11
11
  public id!: string;
@@ -35,15 +35,15 @@ export class SecurityProfileDoc extends plugins.smartdata.SmartDataDbDoc<Securit
35
35
  super();
36
36
  }
37
37
 
38
- public static async findById(id: string): Promise<SecurityProfileDoc | null> {
39
- return await SecurityProfileDoc.getInstance({ id });
38
+ public static async findById(id: string): Promise<SourceProfileDoc | null> {
39
+ return await SourceProfileDoc.getInstance({ id });
40
40
  }
41
41
 
42
- public static async findByName(name: string): Promise<SecurityProfileDoc | null> {
43
- return await SecurityProfileDoc.getInstance({ name });
42
+ public static async findByName(name: string): Promise<SourceProfileDoc | null> {
43
+ return await SourceProfileDoc.getInstance({ name });
44
44
  }
45
45
 
46
- public static async findAll(): Promise<SecurityProfileDoc[]> {
47
- return await SecurityProfileDoc.getInstances({});
46
+ public static async findAll(): Promise<SourceProfileDoc[]> {
47
+ return await SourceProfileDoc.getInstances({});
48
48
  }
49
49
  }