import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import type { StorageManager } from '../storage/index.js'; import type { IStoredRoute, IRouteOverride, IMergedRoute, IRouteWarning, } from '../../ts_interfaces/data/route-management.js'; const ROUTES_PREFIX = '/config-api/routes/'; const OVERRIDES_PREFIX = '/config-api/overrides/'; export class RouteConfigManager { private storedRoutes = new Map(); private overrides = new Map(); private warnings: IRouteWarning[] = []; constructor( private storageManager: StorageManager, private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[], private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined, ) {} /** * 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); await this.storageManager.delete(`${ROUTES_PREFIX}${id}.json`); 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); await this.storageManager.setJSON(`${OVERRIDES_PREFIX}${routeName}.json`, override); this.computeWarnings(); await this.applyRoutes(); } public async removeOverride(routeName: string): Promise { if (!this.overrides.has(routeName)) return false; this.overrides.delete(routeName); await this.storageManager.delete(`${OVERRIDES_PREFIX}${routeName}.json`); this.computeWarnings(); await this.applyRoutes(); return true; } // ========================================================================= // Private: persistence // ========================================================================= private async loadStoredRoutes(): Promise { const keys = await this.storageManager.list(ROUTES_PREFIX); for (const key of keys) { if (!key.endsWith('.json')) continue; const stored = await this.storageManager.getJSON(key); if (stored?.id) { this.storedRoutes.set(stored.id, stored); } } if (this.storedRoutes.size > 0) { logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`); } } private async loadOverrides(): Promise { const keys = await this.storageManager.list(OVERRIDES_PREFIX); for (const key of keys) { if (!key.endsWith('.json')) continue; const override = await this.storageManager.getJSON(key); if (override?.routeName) { this.overrides.set(override.routeName, override); } } if (this.overrides.size > 0) { logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`); } } private async persistRoute(stored: IStoredRoute): Promise { await this.storageManager.setJSON(`${ROUTES_PREFIX}${stored.id}.json`, stored); } // ========================================================================= // 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 // ========================================================================= private async applyRoutes(): Promise { const smartProxy = this.getSmartProxy(); if (!smartProxy) return; const enabledRoutes: plugins.smartproxy.IRouteConfig[] = []; // Add enabled hardcoded routes (respecting overrides) 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(route); } // Add enabled programmatic routes for (const stored of this.storedRoutes.values()) { if (stored.enabled) { enabledRoutes.push(stored.route); } } await smartProxy.updateRoutes(enabledRoutes); logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`); } }