feat: marketplace modules and isolated container management
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user