* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
73 lines
3.3 KiB
JavaScript
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 }));
|
|
}
|
|
}
|