feat(selfhost): show sanitized update logs in admin UI
This commit is contained in:
@@ -13,7 +13,8 @@ const errors = {
|
||||
COMPOSE_REVIEW_REQUIRED: "Die Compose-Datei muss zuerst auf dem Host geprüft und übernommen werden.",
|
||||
RECOVERY_REQUIRED: "Ein vorheriges Update benötigt Wiederherstellung auf dem Host.",
|
||||
PLAN_CHANGED: "Release oder Konfiguration haben sich geändert. Bitte erneut prüfen.",
|
||||
UPDATE_FAILED: "Auftrag fehlgeschlagen. Geschützte Protokolle auf dem Host prüfen.",
|
||||
UPDATE_FAILED: "Auftrag fehlgeschlagen.",
|
||||
LOGS_UNAVAILABLE: "Das Update-Protokoll kann gerade nicht gelesen werden.",
|
||||
INTERRUPTED: "Der Auftrag wurde unterbrochen. Status auf dem Host prüfen.",
|
||||
UPDATER_UNAVAILABLE: "Der Host-Update-Dienst ist nicht erreichbar. Dienst und Socket-Einbindung prüfen.",
|
||||
UPDATER_PROTOCOL: "Der Host-Update-Dienst hat eine ungültige Antwort geliefert.",
|
||||
@@ -46,12 +47,17 @@ const job = state.extend({ plan: plan.optional(), progress: state.optional(),
|
||||
});
|
||||
const status = z.object({ apiVersion: z.literal(1), acceptingRequests: z.boolean(), engine: state,
|
||||
installed: state.pick({ version: true, revision: true }), request: job.optional(),
|
||||
capabilities: z.object({ requestLogs: z.boolean() }).optional(),
|
||||
});
|
||||
const logs = z.object({ requestId, logs: z.array(z.object({
|
||||
source: z.enum(["service", "events", "commands"]), content: z.string().max(6000), truncated: z.boolean(),
|
||||
})).max(3),
|
||||
});
|
||||
|
||||
// No caller-controlled host, URL, socket, headers or command can reach the privileged service.
|
||||
export function createUpdaterClient(socketPath = "/run/fedeo-updater/control.sock", timeoutMs = 5000) {
|
||||
return (method: "GET" | "POST", path: string, body?: unknown): Promise<{ status: number; body: unknown }> => {
|
||||
if (!((method === "GET" && (path === "/v1/status" || /^\/v1\/requests\/[A-Za-z0-9][A-Za-z0-9_-]{7,79}$/.test(path)))
|
||||
if (!((method === "GET" && (path === "/v1/status" || /^\/v1\/requests\/[A-Za-z0-9][A-Za-z0-9_-]{7,79}(?:\/logs)?$/.test(path)))
|
||||
|| (method === "POST" && ["/v1/check", "/v1/apply"].includes(path)))) {
|
||||
return Promise.reject(new UpdaterError("INVALID_REQUEST", 400));
|
||||
}
|
||||
@@ -76,7 +82,12 @@ export function createUpdaterClient(socketPath = "/run/fedeo-updater/control.soc
|
||||
throw knownError(code) && [400, 404, 409, 503].includes(res.statusCode!)
|
||||
? new UpdaterError(code, res.statusCode) : new UpdaterError("UPDATER_PROTOCOL", 502);
|
||||
}
|
||||
resolve({ status: res.statusCode!, body: (path === "/v1/status" ? status : job).parse(data) });
|
||||
const schema = path === "/v1/status" ? status : path.endsWith("/logs") ? logs : job;
|
||||
const parsed = schema.parse(data);
|
||||
if (path.endsWith("/logs") && data.requestId !== path.split("/")[3]) {
|
||||
throw new UpdaterError("UPDATER_PROTOCOL", 502);
|
||||
}
|
||||
resolve({ status: res.statusCode!, body: parsed });
|
||||
} catch (error) {
|
||||
reject(error instanceof UpdaterError ? error : new UpdaterError("UPDATER_PROTOCOL", 502));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user