feat: marketplace modules and isolated container management
This commit is contained in:
6
modules/demo/Dockerfile
Normal file
6
modules/demo/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY --chown=node:node . .
|
||||
USER node
|
||||
EXPOSE 41001
|
||||
CMD ["node", "backend/server.js"]
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
8
modules/demo/compose.yml
Normal file
8
modules/demo/compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
expose:
|
||||
- "41001"
|
||||
restart: unless-stopped
|
||||
@@ -9,5 +9,7 @@
|
||||
"entrypoint": "server.js",
|
||||
"port": 41001,
|
||||
"healthcheck": "/health",
|
||||
"apiVersion": "v1"
|
||||
}
|
||||
"apiVersion": "v1",
|
||||
"composeFile": "compose.yml",
|
||||
"appService": "app"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user