Files
vendoo/app/kernel/route-registry.mjs
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

73 lines
3.3 KiB
JavaScript

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 }));
}
}