import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import { StoredRouteDoc, RouteOverrideDoc } from '../db/index.js'; import type { IStoredRoute, IRouteOverride, IMergedRoute, IRouteWarning, } from '../../ts_interfaces/data/route-management.js'; import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js'; export class RouteConfigManager { private storedRoutes = new Map(); private overrides = new Map(); private warnings: IRouteWarning[] = []; constructor( private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[], private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined, private getHttp3Config?: () => IHttp3Config | undefined, private getVpnAllowList?: (tags?: string[]) => string[], ) {} /** * Load persisted routes and overrides, compute warnings, apply to SmartProxy. */ public async initialize(): Promise { await this.loadStoredRoutes(); await this.loadOverrides(); this.computeWarnings(); this.logWarnings(); await this.applyRoutes(); } // ========================================================================= // Merged view // ========================================================================= public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } { const merged: IMergedRoute[] = []; // Hardcoded routes for (const route of this.getHardcodedRoutes()) { const name = route.name || ''; const override = this.overrides.get(name); merged.push({ route, source: 'hardcoded', enabled: override ? override.enabled : true, overridden: !!override, }); } // Programmatic routes for (const stored of this.storedRoutes.values()) { merged.push({ route: stored.route, source: 'programmatic', enabled: stored.enabled, overridden: false, storedRouteId: stored.id, createdAt: stored.createdAt, updatedAt: stored.updatedAt, }); } return { routes: merged, warnings: [...this.warnings] }; } // ========================================================================= // Programmatic route CRUD // ========================================================================= public async createRoute( route: plugins.smartproxy.IRouteConfig, createdBy: string, enabled = true, ): Promise { const id = plugins.uuid.v4(); const now = Date.now(); // Ensure route has a name if (!route.name) { route.name = `programmatic-${id.slice(0, 8)}`; } const stored: IStoredRoute = { id, route, enabled, createdAt: now, updatedAt: now, createdBy, }; this.storedRoutes.set(id, stored); await this.persistRoute(stored); await this.applyRoutes(); return id; } public async updateRoute( id: string, patch: { route?: Partial; enabled?: boolean }, ): Promise { const stored = this.storedRoutes.get(id); if (!stored) return false; if (patch.route) { stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig; } if (patch.enabled !== undefined) { stored.enabled = patch.enabled; } stored.updatedAt = Date.now(); await this.persistRoute(stored); await this.applyRoutes(); return true; } public async deleteRoute(id: string): Promise { if (!this.storedRoutes.has(id)) return false; this.storedRoutes.delete(id); const doc = await StoredRouteDoc.findById(id); if (doc) await doc.delete(); await this.applyRoutes(); return true; } public async toggleRoute(id: string, enabled: boolean): Promise { return this.updateRoute(id, { enabled }); } // ========================================================================= // Hardcoded route overrides // ========================================================================= public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise { const override: IRouteOverride = { routeName, enabled, updatedAt: Date.now(), updatedBy, }; this.overrides.set(routeName, override); const existingDoc = await RouteOverrideDoc.findByRouteName(routeName); if (existingDoc) { existingDoc.enabled = override.enabled; existingDoc.updatedAt = override.updatedAt; existingDoc.updatedBy = override.updatedBy; await existingDoc.save(); } else { const doc = new RouteOverrideDoc(); doc.routeName = override.routeName; doc.enabled = override.enabled; doc.updatedAt = override.updatedAt; doc.updatedBy = override.updatedBy; await doc.save(); } this.computeWarnings(); await this.applyRoutes(); } public async removeOverride(routeName: string): Promise { if (!this.overrides.has(routeName)) return false; this.overrides.delete(routeName); const doc = await RouteOverrideDoc.findByRouteName(routeName); if (doc) await doc.delete(); this.computeWarnings(); await this.applyRoutes(); return true; } // ========================================================================= // Private: persistence // ========================================================================= private async loadStoredRoutes(): Promise { const docs = await StoredRouteDoc.findAll(); for (const doc of docs) { if (doc.id) { this.storedRoutes.set(doc.id, { id: doc.id, route: doc.route, enabled: doc.enabled, createdAt: doc.createdAt, updatedAt: doc.updatedAt, createdBy: doc.createdBy, }); } } if (this.storedRoutes.size > 0) { logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`); } } private async loadOverrides(): Promise { const docs = await RouteOverrideDoc.findAll(); for (const doc of docs) { if (doc.routeName) { this.overrides.set(doc.routeName, { routeName: doc.routeName, enabled: doc.enabled, updatedAt: doc.updatedAt, updatedBy: doc.updatedBy, }); } } if (this.overrides.size > 0) { logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`); } } private async persistRoute(stored: IStoredRoute): Promise { const existingDoc = await StoredRouteDoc.findById(stored.id); if (existingDoc) { existingDoc.route = stored.route; existingDoc.enabled = stored.enabled; existingDoc.updatedAt = stored.updatedAt; existingDoc.createdBy = stored.createdBy; await existingDoc.save(); } else { const doc = new StoredRouteDoc(); doc.id = stored.id; doc.route = stored.route; doc.enabled = stored.enabled; doc.createdAt = stored.createdAt; doc.updatedAt = stored.updatedAt; doc.createdBy = stored.createdBy; await doc.save(); } } // ========================================================================= // Private: warnings // ========================================================================= private computeWarnings(): void { this.warnings = []; const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || '')); // Check overrides for (const [routeName, override] of this.overrides) { if (!hardcodedNames.has(routeName)) { this.warnings.push({ type: 'orphaned-override', routeName, message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`, }); } else if (!override.enabled) { this.warnings.push({ type: 'disabled-hardcoded', routeName, message: `Route '${routeName}' is disabled via API override`, }); } } // Check disabled programmatic routes for (const stored of this.storedRoutes.values()) { if (!stored.enabled) { const name = stored.route.name || stored.id; this.warnings.push({ type: 'disabled-programmatic', routeName: name, message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`, }); } } } private logWarnings(): void { for (const w of this.warnings) { logger.log('warn', w.message); } } // ========================================================================= // Private: apply merged routes to SmartProxy // ========================================================================= public async applyRoutes(): Promise { const smartProxy = this.getSmartProxy(); if (!smartProxy) return; const enabledRoutes: plugins.smartproxy.IRouteConfig[] = []; const http3Config = this.getHttp3Config?.(); const vpnAllowList = this.getVpnAllowList; // Helper: inject VPN security into a route if vpn.enabled is set const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => { if (!vpnAllowList) return route; const dcRoute = route as IDcRouterRouteConfig; if (!dcRoute.vpn?.enabled) return route; const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags); const mandatory = dcRoute.vpn.mandatory === true; // defaults to false return { ...route, security: { ...route.security, ipAllowList: mandatory ? allowList : [...(route.security?.ipAllowList || []), ...allowList], }, }; }; // Add enabled hardcoded routes (respecting overrides, with fresh VPN injection) for (const route of this.getHardcodedRoutes()) { const name = route.name || ''; const override = this.overrides.get(name); if (override && !override.enabled) { continue; // Skip disabled hardcoded route } enabledRoutes.push(injectVpn(route)); } // Add enabled programmatic routes (with HTTP/3 and VPN augmentation) for (const stored of this.storedRoutes.values()) { if (stored.enabled) { let route = stored.route; if (http3Config && http3Config.enabled !== false) { route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config }); } enabledRoutes.push(injectVpn(route)); } } await smartProxy.updateRoutes(enabledRoutes); logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`); } }