import * as plugins from '../plugins.js'; import { GatewayClientDoc } from '../db/index.js'; import type { IGatewayClient } from '../../ts_interfaces/data/workhoster.js'; const defaultCapabilities: IGatewayClient['capabilities'] = { readDomains: true, readDnsRecords: true, syncRoutes: true, syncDnsRecords: false, requestCertificates: false, }; export class GatewayClientManager { public async initialize(): Promise {} public async listClients(): Promise { const docs = await GatewayClientDoc.findAll(); return docs.map((doc) => this.toPublicClient(doc)); } public async getClient(id: string): Promise { const doc = await GatewayClientDoc.findById(id); return doc ? this.toPublicClient(doc) : null; } public async createClient(options: { id?: string; type: IGatewayClient['type']; name: string; description?: string; hostnamePatterns?: string[]; allowedRouteTargets?: IGatewayClient['allowedRouteTargets']; capabilities?: IGatewayClient['capabilities']; createdBy: string; }): Promise { const id = this.normalizeId(options.id || `${options.type}-${plugins.uuid.v4()}`); if (!id) { throw new Error('gateway client id is required'); } if (await GatewayClientDoc.findById(id)) { throw new Error('gateway client already exists'); } const now = Date.now(); const doc = new GatewayClientDoc(); doc.id = id; doc.type = options.type; doc.name = options.name.trim(); doc.description = options.description?.trim() || undefined; doc.hostnamePatterns = this.normalizeStringList(options.hostnamePatterns || []); doc.allowedRouteTargets = this.normalizeAllowedRouteTargets(options.allowedRouteTargets || []); doc.capabilities = { ...defaultCapabilities, ...(options.capabilities || {}) }; doc.enabled = true; doc.createdAt = now; doc.updatedAt = now; doc.createdBy = options.createdBy; await doc.save(); return this.toPublicClient(doc); } public async updateClient( id: string, patch: Partial>, ): Promise { const doc = await GatewayClientDoc.findById(id); if (!doc) return null; if (patch.name !== undefined) doc.name = patch.name.trim(); if (patch.description !== undefined) doc.description = patch.description.trim() || undefined; if (patch.hostnamePatterns !== undefined) doc.hostnamePatterns = this.normalizeStringList(patch.hostnamePatterns); if (patch.allowedRouteTargets !== undefined) doc.allowedRouteTargets = this.normalizeAllowedRouteTargets(patch.allowedRouteTargets); if (patch.capabilities !== undefined) doc.capabilities = { ...defaultCapabilities, ...patch.capabilities }; if (patch.enabled !== undefined) doc.enabled = patch.enabled; doc.updatedAt = Date.now(); await doc.save(); return this.toPublicClient(doc); } public async deleteClient(id: string): Promise { const doc = await GatewayClientDoc.findById(id); if (!doc) return false; await doc.delete(); return true; } private normalizeId(id: string): string { return id.trim().toLowerCase().replace(/[^a-z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); } private normalizeStringList(values: string[]): string[] { return values.map((value) => value.trim().toLowerCase()).filter(Boolean); } private normalizeAllowedRouteTargets(targets: IGatewayClient['allowedRouteTargets']): IGatewayClient['allowedRouteTargets'] { return targets .map((target) => ({ host: target.host.trim().toLowerCase(), ports: target.ports.filter((port) => Number.isInteger(port) && port > 0 && port <= 65535), })) .filter((target) => target.host && target.ports.length > 0); } private toPublicClient(doc: GatewayClientDoc): IGatewayClient { return { id: doc.id, type: doc.type, name: doc.name, description: doc.description, hostnamePatterns: doc.hostnamePatterns || [], allowedRouteTargets: doc.allowedRouteTargets || [], capabilities: doc.capabilities || {}, enabled: doc.enabled, createdAt: doc.createdAt, updatedAt: doc.updatedAt, createdBy: doc.createdBy, }; } }