import { PlatformError, platformErrorPayload } from './errors.mjs'; const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']); const ROUTE_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/; export class RouteRegistry { #routes = new Map(); #policyEngine; #moduleRegistry; constructor({ policyEngine, moduleRegistry = null }) { this.#policyEngine = policyEngine; this.#moduleRegistry = moduleRegistry; } register(app, definition) { const method = String(definition.method || '').toLowerCase(); const id = String(definition.id || ''); const path = String(definition.path || ''); if (!ROUTE_ID.test(id)) throw new PlatformError(`Ungültige Route-ID: ${id}`, { code: 'ROUTE_ID_INVALID' }); if (!METHODS.has(method)) throw new PlatformError(`Ungültige HTTP-Methode für ${id}.`, { code: 'ROUTE_METHOD_INVALID' }); if (!path.startsWith('/')) throw new PlatformError(`Ungültiger Routenpfad für ${id}.`, { code: 'ROUTE_PATH_INVALID' }); if (!definition.policy) throw new PlatformError(`Route ${id} benötigt eine explizite Policy.`, { code: 'ROUTE_POLICY_REQUIRED' }); if (!definition.owner) throw new PlatformError(`Route ${id} benötigt einen Modulbesitzer.`, { code: 'ROUTE_OWNER_REQUIRED' }); if (typeof definition.handler !== 'function') throw new PlatformError(`Route ${id} benötigt einen Handler.`, { code: 'ROUTE_HANDLER_REQUIRED' }); const key = `${method.toUpperCase()} ${path}`; if (this.#routes.has(key)) throw new PlatformError(`Route bereits registriert: ${key}`, { code: 'ROUTE_DUPLICATE' }); const contract = Object.freeze({ id, method: method.toUpperCase(), path, owner: String(definition.owner), policy: String(definition.policy), audit: String(definition.audit || ''), input: definition.input ? 'validated' : 'none', rateLimit: String(definition.rateLimit || 'default'), csrf: definition.csrf !== false, stability: String(definition.stability || 'internal'), }); this.#routes.set(key, contract); app[method](path, ...(definition.middleware || []), async (req, res, next) => { try { if (this.#moduleRegistry && !this.#moduleRegistry.isEnabled(contract.owner)) { throw new PlatformError('Dieses Modul ist deaktiviert.', { code: 'MODULE_DISABLED', status: 503, expose: true, details: { module: contract.owner }, }); } const decision = await this.#policyEngine.evaluate(contract.policy, { req, user: req.user, route: contract }); if (!decision.allowed) { throw new PlatformError('Keine Berechtigung für diese Aktion.', { code: 'POLICY_DENIED', status: 403, expose: true, details: { policy: contract.policy, reason: decision.reason }, }); } if (typeof definition.input === 'function') req.validatedBody = definition.input(req.body); await definition.handler(req, res, { route: contract, decision }); } catch (error) { if (res.headersSent) return next(error); const status = Number.isInteger(error?.status) ? error.status : 500; res.status(status).json(platformErrorPayload(error, req.requestId)); } }); return contract; } list() { return [...this.#routes.values()].map(route => ({ ...route })); } }