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

@@ -69,6 +69,8 @@ Der Server verdrahtet automatisch den Vertrag:
## API
### `extractIdentity(headers)`
> This helper only parses untrusted header values. Do not use it as an authorization check. `createModuleServer` verifies the request-bound, per-module signature before exposing the identity to `/api/me` or route handlers. In production it requires the key injected as `MPM_MODULE_IDENTITY_KEY`.
Liest die Benutzer-Identität aus den Gateway-Headern (`x-user-id`,
`x-user-username`, `x-user-display-name`, `x-user-role`). Case-insensitive;
`null`, wenn der Request nicht über den Gateway kam.

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