@serve.zone/dcrouter 12.1.0 → 12.2.1

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 (72) hide show
  1. package/dist_serve/bundle.js +752 -688
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +6 -1
  4. package/dist_ts/classes.dcrouter.js +11 -3
  5. package/dist_ts/config/classes.db-seeder.d.ts +25 -0
  6. package/dist_ts/config/classes.db-seeder.js +69 -0
  7. package/dist_ts/config/classes.reference-resolver.d.ts +80 -0
  8. package/dist_ts/config/classes.reference-resolver.js +482 -0
  9. package/dist_ts/config/classes.route-config-manager.d.ts +13 -3
  10. package/dist_ts/config/classes.route-config-manager.js +53 -3
  11. package/dist_ts/config/index.d.ts +2 -0
  12. package/dist_ts/config/index.js +3 -1
  13. package/dist_ts/db/documents/classes.network-target.doc.d.ts +15 -0
  14. package/dist_ts/db/documents/classes.network-target.doc.js +118 -0
  15. package/dist_ts/db/documents/classes.security-profile.doc.d.ts +16 -0
  16. package/dist_ts/db/documents/classes.security-profile.doc.js +118 -0
  17. package/dist_ts/db/documents/classes.stored-route.doc.d.ts +2 -0
  18. package/dist_ts/db/documents/classes.stored-route.doc.js +8 -2
  19. package/dist_ts/db/documents/index.d.ts +2 -0
  20. package/dist_ts/db/documents/index.js +3 -1
  21. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  22. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  23. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  24. package/dist_ts/opsserver/handlers/index.js +3 -1
  25. package/dist_ts/opsserver/handlers/network-target.handler.d.ts +10 -0
  26. package/dist_ts/opsserver/handlers/network-target.handler.js +117 -0
  27. package/dist_ts/opsserver/handlers/route-management.handler.js +3 -2
  28. package/dist_ts/opsserver/handlers/security-profile.handler.d.ts +10 -0
  29. package/dist_ts/opsserver/handlers/security-profile.handler.js +119 -0
  30. package/dist_ts_interfaces/data/route-management.d.ts +48 -1
  31. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  32. package/dist_ts_interfaces/requests/index.js +3 -1
  33. package/dist_ts_interfaces/requests/network-targets.d.ts +102 -0
  34. package/dist_ts_interfaces/requests/network-targets.js +2 -0
  35. package/dist_ts_interfaces/requests/route-management.d.ts +3 -1
  36. package/dist_ts_interfaces/requests/security-profiles.d.ts +102 -0
  37. package/dist_ts_interfaces/requests/security-profiles.js +2 -0
  38. package/dist_ts_web/00_commitinfo_data.js +1 -1
  39. package/dist_ts_web/appstate.d.ts +43 -0
  40. package/dist_ts_web/appstate.js +176 -2
  41. package/dist_ts_web/elements/index.d.ts +2 -0
  42. package/dist_ts_web/elements/index.js +3 -1
  43. package/dist_ts_web/elements/ops-dashboard.js +13 -1
  44. package/dist_ts_web/elements/ops-view-networktargets.d.ts +17 -0
  45. package/dist_ts_web/elements/ops-view-networktargets.js +247 -0
  46. package/dist_ts_web/elements/ops-view-securityprofiles.d.ts +17 -0
  47. package/dist_ts_web/elements/ops-view-securityprofiles.js +276 -0
  48. package/dist_ts_web/router.d.ts +1 -1
  49. package/dist_ts_web/router.js +2 -2
  50. package/package.json +1 -1
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/classes.dcrouter.ts +19 -1
  53. package/ts/config/classes.db-seeder.ts +95 -0
  54. package/ts/config/classes.reference-resolver.ts +576 -0
  55. package/ts/config/classes.route-config-manager.ts +64 -1
  56. package/ts/config/index.ts +3 -1
  57. package/ts/db/documents/classes.network-target.doc.ts +48 -0
  58. package/ts/db/documents/classes.security-profile.doc.ts +49 -0
  59. package/ts/db/documents/classes.stored-route.doc.ts +4 -0
  60. package/ts/db/documents/index.ts +2 -0
  61. package/ts/opsserver/classes.opsserver.ts +4 -0
  62. package/ts/opsserver/handlers/index.ts +3 -1
  63. package/ts/opsserver/handlers/network-target.handler.ts +167 -0
  64. package/ts/opsserver/handlers/route-management.handler.ts +2 -1
  65. package/ts/opsserver/handlers/security-profile.handler.ts +169 -0
  66. package/ts_web/00_commitinfo_data.ts +1 -1
  67. package/ts_web/appstate.ts +243 -1
  68. package/ts_web/elements/index.ts +2 -0
  69. package/ts_web/elements/ops-dashboard.ts +12 -0
  70. package/ts_web/elements/ops-view-networktargets.ts +215 -0
  71. package/ts_web/elements/ops-view-securityprofiles.ts +243 -0
  72. package/ts_web/router.ts +1 -1
@@ -0,0 +1,576 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { SecurityProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
4
+ import type {
5
+ ISecurityProfile,
6
+ INetworkTarget,
7
+ IRouteMetadata,
8
+ IStoredRoute,
9
+ IRouteSecurity,
10
+ } from '../../ts_interfaces/data/route-management.js';
11
+
12
+ const MAX_INHERITANCE_DEPTH = 5;
13
+
14
+ export class ReferenceResolver {
15
+ private profiles = new Map<string, ISecurityProfile>();
16
+ private targets = new Map<string, INetworkTarget>();
17
+
18
+ // =========================================================================
19
+ // Lifecycle
20
+ // =========================================================================
21
+
22
+ public async initialize(): Promise<void> {
23
+ await this.loadProfiles();
24
+ await this.loadTargets();
25
+ }
26
+
27
+ // =========================================================================
28
+ // Profile CRUD
29
+ // =========================================================================
30
+
31
+ public async createProfile(data: {
32
+ name: string;
33
+ description?: string;
34
+ security: IRouteSecurity;
35
+ extendsProfiles?: string[];
36
+ createdBy: string;
37
+ }): Promise<string> {
38
+ const id = plugins.uuid.v4();
39
+ const now = Date.now();
40
+
41
+ const profile: ISecurityProfile = {
42
+ id,
43
+ name: data.name,
44
+ description: data.description,
45
+ security: data.security,
46
+ extendsProfiles: data.extendsProfiles,
47
+ createdAt: now,
48
+ updatedAt: now,
49
+ createdBy: data.createdBy,
50
+ };
51
+
52
+ this.profiles.set(id, profile);
53
+ await this.persistProfile(profile);
54
+ logger.log('info', `Created security profile '${profile.name}' (${id})`);
55
+ return id;
56
+ }
57
+
58
+ public async updateProfile(
59
+ id: string,
60
+ patch: Partial<Omit<ISecurityProfile, 'id' | 'createdAt' | 'createdBy'>>,
61
+ ): Promise<{ affectedRouteIds: string[] }> {
62
+ const profile = this.profiles.get(id);
63
+ if (!profile) {
64
+ throw new Error(`Security profile '${id}' not found`);
65
+ }
66
+
67
+ if (patch.name !== undefined) profile.name = patch.name;
68
+ if (patch.description !== undefined) profile.description = patch.description;
69
+ if (patch.security !== undefined) profile.security = patch.security;
70
+ if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles;
71
+ profile.updatedAt = Date.now();
72
+
73
+ await this.persistProfile(profile);
74
+ logger.log('info', `Updated security profile '${profile.name}' (${id})`);
75
+
76
+ // Find routes referencing this profile
77
+ const affectedRouteIds = await this.findRoutesByProfileRef(id);
78
+ return { affectedRouteIds };
79
+ }
80
+
81
+ public async deleteProfile(
82
+ id: string,
83
+ force: boolean,
84
+ storedRoutes?: Map<string, IStoredRoute>,
85
+ ): Promise<{ success: boolean; message?: string }> {
86
+ const profile = this.profiles.get(id);
87
+ if (!profile) {
88
+ return { success: false, message: `Security profile '${id}' not found` };
89
+ }
90
+
91
+ // Check usage
92
+ const affectedIds = storedRoutes
93
+ ? this.findRoutesByProfileRefSync(id, storedRoutes)
94
+ : await this.findRoutesByProfileRef(id);
95
+
96
+ if (affectedIds.length > 0 && !force) {
97
+ return {
98
+ success: false,
99
+ message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
100
+ };
101
+ }
102
+
103
+ // Delete from DB
104
+ const doc = await SecurityProfileDoc.findById(id);
105
+ if (doc) await doc.delete();
106
+ this.profiles.delete(id);
107
+
108
+ // If force-deleting with referencing routes, clear refs but keep resolved values
109
+ if (affectedIds.length > 0) {
110
+ await this.clearProfileRefsOnRoutes(affectedIds);
111
+ logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
112
+ } else {
113
+ logger.log('info', `Deleted security profile '${profile.name}' (${id})`);
114
+ }
115
+
116
+ return { success: true };
117
+ }
118
+
119
+ public getProfile(id: string): ISecurityProfile | undefined {
120
+ return this.profiles.get(id);
121
+ }
122
+
123
+ public getProfileByName(name: string): ISecurityProfile | undefined {
124
+ for (const profile of this.profiles.values()) {
125
+ if (profile.name === name) return profile;
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ public listProfiles(): ISecurityProfile[] {
131
+ return [...this.profiles.values()];
132
+ }
133
+
134
+ public getProfileUsage(storedRoutes: Map<string, IStoredRoute>): Map<string, Array<{ id: string; routeName: string }>> {
135
+ const usage = new Map<string, Array<{ id: string; routeName: string }>>();
136
+ for (const profile of this.profiles.values()) {
137
+ usage.set(profile.id, []);
138
+ }
139
+ for (const [routeId, stored] of storedRoutes) {
140
+ const ref = stored.metadata?.securityProfileRef;
141
+ if (ref && usage.has(ref)) {
142
+ usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
143
+ }
144
+ }
145
+ return usage;
146
+ }
147
+
148
+ public getProfileUsageForId(
149
+ profileId: string,
150
+ storedRoutes: Map<string, IStoredRoute>,
151
+ ): Array<{ id: string; routeName: string }> {
152
+ const routes: Array<{ id: string; routeName: string }> = [];
153
+ for (const [routeId, stored] of storedRoutes) {
154
+ if (stored.metadata?.securityProfileRef === profileId) {
155
+ routes.push({ id: routeId, routeName: stored.route.name || routeId });
156
+ }
157
+ }
158
+ return routes;
159
+ }
160
+
161
+ // =========================================================================
162
+ // Target CRUD
163
+ // =========================================================================
164
+
165
+ public async createTarget(data: {
166
+ name: string;
167
+ description?: string;
168
+ host: string | string[];
169
+ port: number;
170
+ createdBy: string;
171
+ }): Promise<string> {
172
+ const id = plugins.uuid.v4();
173
+ const now = Date.now();
174
+
175
+ const target: INetworkTarget = {
176
+ id,
177
+ name: data.name,
178
+ description: data.description,
179
+ host: data.host,
180
+ port: data.port,
181
+ createdAt: now,
182
+ updatedAt: now,
183
+ createdBy: data.createdBy,
184
+ };
185
+
186
+ this.targets.set(id, target);
187
+ await this.persistTarget(target);
188
+ logger.log('info', `Created network target '${target.name}' (${id})`);
189
+ return id;
190
+ }
191
+
192
+ public async updateTarget(
193
+ id: string,
194
+ patch: Partial<Omit<INetworkTarget, 'id' | 'createdAt' | 'createdBy'>>,
195
+ ): Promise<{ affectedRouteIds: string[] }> {
196
+ const target = this.targets.get(id);
197
+ if (!target) {
198
+ throw new Error(`Network target '${id}' not found`);
199
+ }
200
+
201
+ if (patch.name !== undefined) target.name = patch.name;
202
+ if (patch.description !== undefined) target.description = patch.description;
203
+ if (patch.host !== undefined) target.host = patch.host;
204
+ if (patch.port !== undefined) target.port = patch.port;
205
+ target.updatedAt = Date.now();
206
+
207
+ await this.persistTarget(target);
208
+ logger.log('info', `Updated network target '${target.name}' (${id})`);
209
+
210
+ const affectedRouteIds = await this.findRoutesByTargetRef(id);
211
+ return { affectedRouteIds };
212
+ }
213
+
214
+ public async deleteTarget(
215
+ id: string,
216
+ force: boolean,
217
+ storedRoutes?: Map<string, IStoredRoute>,
218
+ ): Promise<{ success: boolean; message?: string }> {
219
+ const target = this.targets.get(id);
220
+ if (!target) {
221
+ return { success: false, message: `Network target '${id}' not found` };
222
+ }
223
+
224
+ const affectedIds = storedRoutes
225
+ ? this.findRoutesByTargetRefSync(id, storedRoutes)
226
+ : await this.findRoutesByTargetRef(id);
227
+
228
+ if (affectedIds.length > 0 && !force) {
229
+ return {
230
+ success: false,
231
+ message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
232
+ };
233
+ }
234
+
235
+ const doc = await NetworkTargetDoc.findById(id);
236
+ if (doc) await doc.delete();
237
+ this.targets.delete(id);
238
+
239
+ if (affectedIds.length > 0) {
240
+ await this.clearTargetRefsOnRoutes(affectedIds);
241
+ logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`);
242
+ } else {
243
+ logger.log('info', `Deleted network target '${target.name}' (${id})`);
244
+ }
245
+
246
+ return { success: true };
247
+ }
248
+
249
+ public getTarget(id: string): INetworkTarget | undefined {
250
+ return this.targets.get(id);
251
+ }
252
+
253
+ public getTargetByName(name: string): INetworkTarget | undefined {
254
+ for (const target of this.targets.values()) {
255
+ if (target.name === name) return target;
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ public listTargets(): INetworkTarget[] {
261
+ return [...this.targets.values()];
262
+ }
263
+
264
+ public getTargetUsageForId(
265
+ targetId: string,
266
+ storedRoutes: Map<string, IStoredRoute>,
267
+ ): Array<{ id: string; routeName: string }> {
268
+ const routes: Array<{ id: string; routeName: string }> = [];
269
+ for (const [routeId, stored] of storedRoutes) {
270
+ if (stored.metadata?.networkTargetRef === targetId) {
271
+ routes.push({ id: routeId, routeName: stored.route.name || routeId });
272
+ }
273
+ }
274
+ return routes;
275
+ }
276
+
277
+ // =========================================================================
278
+ // Resolution
279
+ // =========================================================================
280
+
281
+ /**
282
+ * Resolve references for a single route.
283
+ * Materializes security profile and/or network target into the route's fields.
284
+ * Returns the resolved route and updated metadata.
285
+ */
286
+ public resolveRoute(
287
+ route: plugins.smartproxy.IRouteConfig,
288
+ metadata?: IRouteMetadata,
289
+ ): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
290
+ const resolvedMetadata: IRouteMetadata = { ...metadata };
291
+
292
+ if (resolvedMetadata.securityProfileRef) {
293
+ const resolvedSecurity = this.resolveSecurityProfile(resolvedMetadata.securityProfileRef);
294
+ if (resolvedSecurity) {
295
+ const profile = this.profiles.get(resolvedMetadata.securityProfileRef);
296
+ // Merge: profile provides base, route's inline values override
297
+ route = {
298
+ ...route,
299
+ security: this.mergeSecurityFields(resolvedSecurity, route.security),
300
+ };
301
+ resolvedMetadata.securityProfileName = profile?.name;
302
+ resolvedMetadata.lastResolvedAt = Date.now();
303
+ } else {
304
+ logger.log('warn', `Security profile '${resolvedMetadata.securityProfileRef}' not found during resolution`);
305
+ }
306
+ }
307
+
308
+ if (resolvedMetadata.networkTargetRef) {
309
+ const target = this.targets.get(resolvedMetadata.networkTargetRef);
310
+ if (target) {
311
+ route = {
312
+ ...route,
313
+ action: {
314
+ ...route.action,
315
+ targets: [{
316
+ host: target.host as string,
317
+ port: target.port,
318
+ }],
319
+ },
320
+ };
321
+ resolvedMetadata.networkTargetName = target.name;
322
+ resolvedMetadata.lastResolvedAt = Date.now();
323
+ } else {
324
+ logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`);
325
+ }
326
+ }
327
+
328
+ return { route, metadata: resolvedMetadata };
329
+ }
330
+
331
+ // =========================================================================
332
+ // Reference lookup helpers
333
+ // =========================================================================
334
+
335
+ public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
336
+ const docs = await StoredRouteDoc.findAll();
337
+ return docs
338
+ .filter((doc) => doc.metadata?.securityProfileRef === profileId)
339
+ .map((doc) => doc.id);
340
+ }
341
+
342
+ public async findRoutesByTargetRef(targetId: string): Promise<string[]> {
343
+ const docs = await StoredRouteDoc.findAll();
344
+ return docs
345
+ .filter((doc) => doc.metadata?.networkTargetRef === targetId)
346
+ .map((doc) => doc.id);
347
+ }
348
+
349
+ public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
350
+ const ids: string[] = [];
351
+ for (const [routeId, stored] of storedRoutes) {
352
+ if (stored.metadata?.securityProfileRef === profileId) {
353
+ ids.push(routeId);
354
+ }
355
+ }
356
+ return ids;
357
+ }
358
+
359
+ public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
360
+ const ids: string[] = [];
361
+ for (const [routeId, stored] of storedRoutes) {
362
+ if (stored.metadata?.networkTargetRef === targetId) {
363
+ ids.push(routeId);
364
+ }
365
+ }
366
+ return ids;
367
+ }
368
+
369
+ // =========================================================================
370
+ // Private: security profile resolution with inheritance
371
+ // =========================================================================
372
+
373
+ private resolveSecurityProfile(
374
+ profileId: string,
375
+ visited: Set<string> = new Set(),
376
+ depth: number = 0,
377
+ ): IRouteSecurity | null {
378
+ if (depth > MAX_INHERITANCE_DEPTH) {
379
+ logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`);
380
+ return null;
381
+ }
382
+
383
+ if (visited.has(profileId)) {
384
+ logger.log('warn', `Circular inheritance detected for profile '${profileId}'`);
385
+ return null;
386
+ }
387
+
388
+ const profile = this.profiles.get(profileId);
389
+ if (!profile) return null;
390
+
391
+ visited.add(profileId);
392
+
393
+ // Start with an empty base
394
+ let baseSecurity: IRouteSecurity = {};
395
+
396
+ // Resolve parent profiles first (top-down, later overrides earlier)
397
+ if (profile.extendsProfiles?.length) {
398
+ for (const parentId of profile.extendsProfiles) {
399
+ const parentSecurity = this.resolveSecurityProfile(parentId, new Set(visited), depth + 1);
400
+ if (parentSecurity) {
401
+ baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
402
+ }
403
+ }
404
+ }
405
+
406
+ // Apply this profile's security on top
407
+ return this.mergeSecurityFields(baseSecurity, profile.security);
408
+ }
409
+
410
+ /**
411
+ * Merge two IRouteSecurity objects.
412
+ * `override` values take precedence over `base` values.
413
+ * For ipAllowList/ipBlockList: union arrays and deduplicate.
414
+ * For scalar/object fields: override wins if present.
415
+ */
416
+ private mergeSecurityFields(
417
+ base: IRouteSecurity | undefined,
418
+ override: IRouteSecurity | undefined,
419
+ ): IRouteSecurity {
420
+ if (!base && !override) return {};
421
+ if (!base) return { ...override };
422
+ if (!override) return { ...base };
423
+
424
+ const merged: IRouteSecurity = { ...base };
425
+
426
+ // IP lists: union
427
+ if (override.ipAllowList || base.ipAllowList) {
428
+ merged.ipAllowList = [...new Set([
429
+ ...(base.ipAllowList || []),
430
+ ...(override.ipAllowList || []),
431
+ ])];
432
+ }
433
+
434
+ if (override.ipBlockList || base.ipBlockList) {
435
+ merged.ipBlockList = [...new Set([
436
+ ...(base.ipBlockList || []),
437
+ ...(override.ipBlockList || []),
438
+ ])];
439
+ }
440
+
441
+ // Scalar/object fields: override wins
442
+ if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections;
443
+ if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit;
444
+ if (override.authentication !== undefined) merged.authentication = override.authentication;
445
+ if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
446
+ if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
447
+
448
+ return merged;
449
+ }
450
+
451
+ // =========================================================================
452
+ // Private: persistence
453
+ // =========================================================================
454
+
455
+ private async loadProfiles(): Promise<void> {
456
+ const docs = await SecurityProfileDoc.findAll();
457
+ for (const doc of docs) {
458
+ if (doc.id) {
459
+ this.profiles.set(doc.id, {
460
+ id: doc.id,
461
+ name: doc.name,
462
+ description: doc.description,
463
+ security: doc.security,
464
+ extendsProfiles: doc.extendsProfiles,
465
+ createdAt: doc.createdAt,
466
+ updatedAt: doc.updatedAt,
467
+ createdBy: doc.createdBy,
468
+ });
469
+ }
470
+ }
471
+ if (this.profiles.size > 0) {
472
+ logger.log('info', `Loaded ${this.profiles.size} security profile(s) from storage`);
473
+ }
474
+ }
475
+
476
+ private async loadTargets(): Promise<void> {
477
+ const docs = await NetworkTargetDoc.findAll();
478
+ for (const doc of docs) {
479
+ if (doc.id) {
480
+ this.targets.set(doc.id, {
481
+ id: doc.id,
482
+ name: doc.name,
483
+ description: doc.description,
484
+ host: doc.host,
485
+ port: doc.port,
486
+ createdAt: doc.createdAt,
487
+ updatedAt: doc.updatedAt,
488
+ createdBy: doc.createdBy,
489
+ });
490
+ }
491
+ }
492
+ if (this.targets.size > 0) {
493
+ logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`);
494
+ }
495
+ }
496
+
497
+ private async persistProfile(profile: ISecurityProfile): Promise<void> {
498
+ const existingDoc = await SecurityProfileDoc.findById(profile.id);
499
+ if (existingDoc) {
500
+ existingDoc.name = profile.name;
501
+ existingDoc.description = profile.description;
502
+ existingDoc.security = profile.security;
503
+ existingDoc.extendsProfiles = profile.extendsProfiles;
504
+ existingDoc.updatedAt = profile.updatedAt;
505
+ await existingDoc.save();
506
+ } else {
507
+ const doc = new SecurityProfileDoc();
508
+ doc.id = profile.id;
509
+ doc.name = profile.name;
510
+ doc.description = profile.description;
511
+ doc.security = profile.security;
512
+ doc.extendsProfiles = profile.extendsProfiles;
513
+ doc.createdAt = profile.createdAt;
514
+ doc.updatedAt = profile.updatedAt;
515
+ doc.createdBy = profile.createdBy;
516
+ await doc.save();
517
+ }
518
+ }
519
+
520
+ private async persistTarget(target: INetworkTarget): Promise<void> {
521
+ const existingDoc = await NetworkTargetDoc.findById(target.id);
522
+ if (existingDoc) {
523
+ existingDoc.name = target.name;
524
+ existingDoc.description = target.description;
525
+ existingDoc.host = target.host;
526
+ existingDoc.port = target.port;
527
+ existingDoc.updatedAt = target.updatedAt;
528
+ await existingDoc.save();
529
+ } else {
530
+ const doc = new NetworkTargetDoc();
531
+ doc.id = target.id;
532
+ doc.name = target.name;
533
+ doc.description = target.description;
534
+ doc.host = target.host;
535
+ doc.port = target.port;
536
+ doc.createdAt = target.createdAt;
537
+ doc.updatedAt = target.updatedAt;
538
+ doc.createdBy = target.createdBy;
539
+ await doc.save();
540
+ }
541
+ }
542
+
543
+ // =========================================================================
544
+ // Private: ref cleanup on force-delete
545
+ // =========================================================================
546
+
547
+ private async clearProfileRefsOnRoutes(routeIds: string[]): Promise<void> {
548
+ for (const routeId of routeIds) {
549
+ const doc = await StoredRouteDoc.findById(routeId);
550
+ if (doc?.metadata) {
551
+ doc.metadata = {
552
+ ...doc.metadata,
553
+ securityProfileRef: undefined,
554
+ securityProfileName: undefined,
555
+ };
556
+ doc.updatedAt = Date.now();
557
+ await doc.save();
558
+ }
559
+ }
560
+ }
561
+
562
+ private async clearTargetRefsOnRoutes(routeIds: string[]): Promise<void> {
563
+ for (const routeId of routeIds) {
564
+ const doc = await StoredRouteDoc.findById(routeId);
565
+ if (doc?.metadata) {
566
+ doc.metadata = {
567
+ ...doc.metadata,
568
+ networkTargetRef: undefined,
569
+ networkTargetName: undefined,
570
+ };
571
+ doc.updatedAt = Date.now();
572
+ await doc.save();
573
+ }
574
+ }
575
+ }
576
+ }