import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import { SecurityProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js'; import type { ISecurityProfile, INetworkTarget, IRouteMetadata, IStoredRoute, IRouteSecurity, } from '../../ts_interfaces/data/route-management.js'; const MAX_INHERITANCE_DEPTH = 5; export class ReferenceResolver { private profiles = new Map(); private targets = new Map(); // ========================================================================= // Lifecycle // ========================================================================= public async initialize(): Promise { await this.loadProfiles(); await this.loadTargets(); } // ========================================================================= // Profile CRUD // ========================================================================= public async createProfile(data: { name: string; description?: string; security: IRouteSecurity; extendsProfiles?: string[]; createdBy: string; }): Promise { const id = plugins.uuid.v4(); const now = Date.now(); const profile: ISecurityProfile = { id, name: data.name, description: data.description, security: data.security, extendsProfiles: data.extendsProfiles, createdAt: now, updatedAt: now, createdBy: data.createdBy, }; this.profiles.set(id, profile); await this.persistProfile(profile); logger.log('info', `Created security profile '${profile.name}' (${id})`); return id; } public async updateProfile( id: string, patch: Partial>, ): Promise<{ affectedRouteIds: string[] }> { const profile = this.profiles.get(id); if (!profile) { throw new Error(`Security profile '${id}' not found`); } if (patch.name !== undefined) profile.name = patch.name; if (patch.description !== undefined) profile.description = patch.description; if (patch.security !== undefined) profile.security = patch.security; if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles; profile.updatedAt = Date.now(); await this.persistProfile(profile); logger.log('info', `Updated security profile '${profile.name}' (${id})`); // Find routes referencing this profile const affectedRouteIds = await this.findRoutesByProfileRef(id); return { affectedRouteIds }; } public async deleteProfile( id: string, force: boolean, storedRoutes?: Map, ): Promise<{ success: boolean; message?: string }> { const profile = this.profiles.get(id); if (!profile) { return { success: false, message: `Security profile '${id}' not found` }; } // Check usage const affectedIds = storedRoutes ? this.findRoutesByProfileRefSync(id, storedRoutes) : await this.findRoutesByProfileRef(id); if (affectedIds.length > 0 && !force) { return { success: false, message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`, }; } // Delete from DB const doc = await SecurityProfileDoc.findById(id); if (doc) await doc.delete(); this.profiles.delete(id); // If force-deleting with referencing routes, clear refs but keep resolved values if (affectedIds.length > 0) { await this.clearProfileRefsOnRoutes(affectedIds); logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`); } else { logger.log('info', `Deleted security profile '${profile.name}' (${id})`); } return { success: true }; } public getProfile(id: string): ISecurityProfile | undefined { return this.profiles.get(id); } public getProfileByName(name: string): ISecurityProfile | undefined { for (const profile of this.profiles.values()) { if (profile.name === name) return profile; } return undefined; } public listProfiles(): ISecurityProfile[] { return [...this.profiles.values()]; } public getProfileUsage(storedRoutes: Map): Map> { const usage = new Map>(); for (const profile of this.profiles.values()) { usage.set(profile.id, []); } for (const [routeId, stored] of storedRoutes) { const ref = stored.metadata?.securityProfileRef; if (ref && usage.has(ref)) { usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId }); } } return usage; } public getProfileUsageForId( profileId: string, storedRoutes: Map, ): Array<{ id: string; routeName: string }> { const routes: Array<{ id: string; routeName: string }> = []; for (const [routeId, stored] of storedRoutes) { if (stored.metadata?.securityProfileRef === profileId) { routes.push({ id: routeId, routeName: stored.route.name || routeId }); } } return routes; } // ========================================================================= // Target CRUD // ========================================================================= public async createTarget(data: { name: string; description?: string; host: string | string[]; port: number; createdBy: string; }): Promise { const id = plugins.uuid.v4(); const now = Date.now(); const target: INetworkTarget = { id, name: data.name, description: data.description, host: data.host, port: data.port, createdAt: now, updatedAt: now, createdBy: data.createdBy, }; this.targets.set(id, target); await this.persistTarget(target); logger.log('info', `Created network target '${target.name}' (${id})`); return id; } public async updateTarget( id: string, patch: Partial>, ): Promise<{ affectedRouteIds: string[] }> { const target = this.targets.get(id); if (!target) { throw new Error(`Network target '${id}' not found`); } if (patch.name !== undefined) target.name = patch.name; if (patch.description !== undefined) target.description = patch.description; if (patch.host !== undefined) target.host = patch.host; if (patch.port !== undefined) target.port = patch.port; target.updatedAt = Date.now(); await this.persistTarget(target); logger.log('info', `Updated network target '${target.name}' (${id})`); const affectedRouteIds = await this.findRoutesByTargetRef(id); return { affectedRouteIds }; } public async deleteTarget( id: string, force: boolean, storedRoutes?: Map, ): Promise<{ success: boolean; message?: string }> { const target = this.targets.get(id); if (!target) { return { success: false, message: `Network target '${id}' not found` }; } const affectedIds = storedRoutes ? this.findRoutesByTargetRefSync(id, storedRoutes) : await this.findRoutesByTargetRef(id); if (affectedIds.length > 0 && !force) { return { success: false, message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`, }; } const doc = await NetworkTargetDoc.findById(id); if (doc) await doc.delete(); this.targets.delete(id); if (affectedIds.length > 0) { await this.clearTargetRefsOnRoutes(affectedIds); logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`); } else { logger.log('info', `Deleted network target '${target.name}' (${id})`); } return { success: true }; } public getTarget(id: string): INetworkTarget | undefined { return this.targets.get(id); } public getTargetByName(name: string): INetworkTarget | undefined { for (const target of this.targets.values()) { if (target.name === name) return target; } return undefined; } public listTargets(): INetworkTarget[] { return [...this.targets.values()]; } public getTargetUsageForId( targetId: string, storedRoutes: Map, ): Array<{ id: string; routeName: string }> { const routes: Array<{ id: string; routeName: string }> = []; for (const [routeId, stored] of storedRoutes) { if (stored.metadata?.networkTargetRef === targetId) { routes.push({ id: routeId, routeName: stored.route.name || routeId }); } } return routes; } // ========================================================================= // Resolution // ========================================================================= /** * Resolve references for a single route. * Materializes security profile and/or network target into the route's fields. * Returns the resolved route and updated metadata. */ public resolveRoute( route: plugins.smartproxy.IRouteConfig, metadata?: IRouteMetadata, ): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } { const resolvedMetadata: IRouteMetadata = { ...metadata }; if (resolvedMetadata.securityProfileRef) { const resolvedSecurity = this.resolveSecurityProfile(resolvedMetadata.securityProfileRef); if (resolvedSecurity) { const profile = this.profiles.get(resolvedMetadata.securityProfileRef); // Merge: profile provides base, route's inline values override route = { ...route, security: this.mergeSecurityFields(resolvedSecurity, route.security), }; resolvedMetadata.securityProfileName = profile?.name; resolvedMetadata.lastResolvedAt = Date.now(); } else { logger.log('warn', `Security profile '${resolvedMetadata.securityProfileRef}' not found during resolution`); } } if (resolvedMetadata.networkTargetRef) { const target = this.targets.get(resolvedMetadata.networkTargetRef); if (target) { route = { ...route, action: { ...route.action, targets: [{ host: target.host as string, port: target.port, }], }, }; resolvedMetadata.networkTargetName = target.name; resolvedMetadata.lastResolvedAt = Date.now(); } else { logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`); } } return { route, metadata: resolvedMetadata }; } // ========================================================================= // Reference lookup helpers // ========================================================================= public async findRoutesByProfileRef(profileId: string): Promise { const docs = await StoredRouteDoc.findAll(); return docs .filter((doc) => doc.metadata?.securityProfileRef === profileId) .map((doc) => doc.id); } public async findRoutesByTargetRef(targetId: string): Promise { const docs = await StoredRouteDoc.findAll(); return docs .filter((doc) => doc.metadata?.networkTargetRef === targetId) .map((doc) => doc.id); } public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map): string[] { const ids: string[] = []; for (const [routeId, stored] of storedRoutes) { if (stored.metadata?.securityProfileRef === profileId) { ids.push(routeId); } } return ids; } public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map): string[] { const ids: string[] = []; for (const [routeId, stored] of storedRoutes) { if (stored.metadata?.networkTargetRef === targetId) { ids.push(routeId); } } return ids; } // ========================================================================= // Private: security profile resolution with inheritance // ========================================================================= private resolveSecurityProfile( profileId: string, visited: Set = new Set(), depth: number = 0, ): IRouteSecurity | null { if (depth > MAX_INHERITANCE_DEPTH) { logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`); return null; } if (visited.has(profileId)) { logger.log('warn', `Circular inheritance detected for profile '${profileId}'`); return null; } const profile = this.profiles.get(profileId); if (!profile) return null; visited.add(profileId); // Start with an empty base let baseSecurity: IRouteSecurity = {}; // Resolve parent profiles first (top-down, later overrides earlier) if (profile.extendsProfiles?.length) { for (const parentId of profile.extendsProfiles) { const parentSecurity = this.resolveSecurityProfile(parentId, new Set(visited), depth + 1); if (parentSecurity) { baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity); } } } // Apply this profile's security on top return this.mergeSecurityFields(baseSecurity, profile.security); } /** * Merge two IRouteSecurity objects. * `override` values take precedence over `base` values. * For ipAllowList/ipBlockList: union arrays and deduplicate. * For scalar/object fields: override wins if present. */ private mergeSecurityFields( base: IRouteSecurity | undefined, override: IRouteSecurity | undefined, ): IRouteSecurity { if (!base && !override) return {}; if (!base) return { ...override }; if (!override) return { ...base }; const merged: IRouteSecurity = { ...base }; // IP lists: union if (override.ipAllowList || base.ipAllowList) { merged.ipAllowList = [...new Set([ ...(base.ipAllowList || []), ...(override.ipAllowList || []), ])]; } if (override.ipBlockList || base.ipBlockList) { merged.ipBlockList = [...new Set([ ...(base.ipBlockList || []), ...(override.ipBlockList || []), ])]; } // Scalar/object fields: override wins if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections; if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit; if (override.authentication !== undefined) merged.authentication = override.authentication; if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth; if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth; return merged; } // ========================================================================= // Private: persistence // ========================================================================= private async loadProfiles(): Promise { const docs = await SecurityProfileDoc.findAll(); for (const doc of docs) { if (doc.id) { this.profiles.set(doc.id, { id: doc.id, name: doc.name, description: doc.description, security: doc.security, extendsProfiles: doc.extendsProfiles, createdAt: doc.createdAt, updatedAt: doc.updatedAt, createdBy: doc.createdBy, }); } } if (this.profiles.size > 0) { logger.log('info', `Loaded ${this.profiles.size} security profile(s) from storage`); } } private async loadTargets(): Promise { const docs = await NetworkTargetDoc.findAll(); for (const doc of docs) { if (doc.id) { this.targets.set(doc.id, { id: doc.id, name: doc.name, description: doc.description, host: doc.host, port: doc.port, createdAt: doc.createdAt, updatedAt: doc.updatedAt, createdBy: doc.createdBy, }); } } if (this.targets.size > 0) { logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`); } } private async persistProfile(profile: ISecurityProfile): Promise { const existingDoc = await SecurityProfileDoc.findById(profile.id); if (existingDoc) { existingDoc.name = profile.name; existingDoc.description = profile.description; existingDoc.security = profile.security; existingDoc.extendsProfiles = profile.extendsProfiles; existingDoc.updatedAt = profile.updatedAt; await existingDoc.save(); } else { const doc = new SecurityProfileDoc(); doc.id = profile.id; doc.name = profile.name; doc.description = profile.description; doc.security = profile.security; doc.extendsProfiles = profile.extendsProfiles; doc.createdAt = profile.createdAt; doc.updatedAt = profile.updatedAt; doc.createdBy = profile.createdBy; await doc.save(); } } private async persistTarget(target: INetworkTarget): Promise { const existingDoc = await NetworkTargetDoc.findById(target.id); if (existingDoc) { existingDoc.name = target.name; existingDoc.description = target.description; existingDoc.host = target.host; existingDoc.port = target.port; existingDoc.updatedAt = target.updatedAt; await existingDoc.save(); } else { const doc = new NetworkTargetDoc(); doc.id = target.id; doc.name = target.name; doc.description = target.description; doc.host = target.host; doc.port = target.port; doc.createdAt = target.createdAt; doc.updatedAt = target.updatedAt; doc.createdBy = target.createdBy; await doc.save(); } } // ========================================================================= // Private: ref cleanup on force-delete // ========================================================================= private async clearProfileRefsOnRoutes(routeIds: string[]): Promise { for (const routeId of routeIds) { const doc = await StoredRouteDoc.findById(routeId); if (doc?.metadata) { doc.metadata = { ...doc.metadata, securityProfileRef: undefined, securityProfileName: undefined, }; doc.updatedAt = Date.now(); await doc.save(); } } } private async clearTargetRefsOnRoutes(routeIds: string[]): Promise { for (const routeId of routeIds) { const doc = await StoredRouteDoc.findById(routeId); if (doc?.metadata) { doc.metadata = { ...doc.metadata, networkTargetRef: undefined, networkTargetName: undefined, }; doc.updatedAt = Date.now(); await doc.save(); } } } }