feat: marketplace modules and isolated container management

This commit is contained in:
ayde64
2026-10-07 23:18:02 +02:00
parent 564f7def1c
commit 7b121fa908
68 changed files with 2560 additions and 699 deletions

View File

@@ -26,6 +26,7 @@
const http = require('node:http');
const fs = require('node:fs');
const { createHmac, timingSafeEqual } = require('node:crypto');
/** Identitäts-Header, die der Modul-Gateway setzt. */
const GATEWAY_HEADERS = Object.freeze({
@@ -82,6 +83,29 @@ function extractIdentity(headers) {
return { userId, username, displayName: displayName ?? username, role };
}
/** Validate the gateway's signature, bound to this module and exact HTTP request. */
function extractVerifiedIdentity(headers, request, moduleId, identityKey) {
const identity = extractIdentity(headers);
const timestamp = readHeader(headers, 'x-mpm-identity-timestamp');
const supplied = readHeader(headers, 'x-mpm-identity-signature');
if (!identity || !timestamp || !supplied || !identityKey) return null;
const timestampNumber = Number(timestamp);
if (!Number.isSafeInteger(timestampNumber) || Math.abs(Date.now() - timestampNumber) > 30_000) {
return null;
}
const fields = [moduleId, request.method, request.url ?? '/', timestamp,
identity.userId, identity.username, identity.displayName, identity.role];
const expected = createHmac('sha256', identityKey).update(JSON.stringify(fields)).digest();
let actual;
try {
actual = Buffer.from(supplied, 'hex');
} catch {
return null;
}
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return null;
return identity;
}
/** Erstellt die /health-Antwort nach Vertrag. */
function healthResponse(moduleId, version) {
return { moduleId, version, status: 'healthy' };
@@ -280,6 +304,15 @@ function createModuleServer(options) {
const permissions = options.permissions ?? [];
const logger = options.logger ?? createLogger({ moduleId: manifest.id });
const identityRequired = options.identityRequired ?? true;
const identityKey = options.identityKey ?? process.env.MPM_MODULE_IDENTITY_KEY ?? null;
const requireSignedIdentity = options.requireSignedIdentity ?? process.env.NODE_ENV === 'production';
function requestIdentity(request) {
if (identityKey) {
return extractVerifiedIdentity(request.headers, request, manifest.id, identityKey);
}
return requireSignedIdentity ? null : extractIdentity(request.headers);
}
function sendJson(response, status, body) {
response.writeHead(status, { 'Content-Type': 'application/json' });
@@ -299,7 +332,7 @@ function createModuleServer(options) {
return;
}
if (request.method === 'GET' && pathname === '/api/me') {
const identity = extractIdentity(request.headers);
const identity = requestIdentity(request);
if (!identity) {
sendJson(response, 401, { statusCode: 401, message: 'Keine Identität übergeben' });
return;
@@ -308,7 +341,7 @@ function createModuleServer(options) {
return;
}
const identity = extractIdentity(request.headers);
const identity = requestIdentity(request);
if (identityRequired && routes && !identity) {
sendJson(response, 401, { statusCode: 401, message: 'Keine Identität übergeben' });
return;
@@ -336,6 +369,7 @@ module.exports = {
GATEWAY_HEADERS,
PLATFORM_ROLES,
extractIdentity,
extractVerifiedIdentity,
healthResponse,
manifestResponse,
meResponse,
@@ -344,4 +378,4 @@ module.exports = {
createLogger,
createPlatformClient,
createModuleServer,
};
};

View File

@@ -35,6 +35,6 @@ const server = createModuleServer({
},
});
server.listen(port, '127.0.0.1', () => {
server.listen(port, '0.0.0.0', () => {
logger.info('Demo-Modul gestartet', { port });
});
});