Files
ProxLedger/src/server/agent/schema.ts
T
florian.wildeandClaude Opus 5 bef51eb863 feat(api): Agent-API mit Enrollment, Report und Ping (#004)
Gehashte Tokens mit Praefix, Rate-Limit je IP und Token, zod-Validierung mit
durchgaengig optionalen Feldern, idempotente Reports ueber reportId, Key- und
Server-ID-Historie sowie automatischer Laufzeit-Vorschlag. 32 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:39:33 +02:00

113 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import type { Produkt, SubscriptionStatus } from "@/generated/prisma/enums";
/**
* Schemas für die Agent-API. Grundregel aus dem Auftrag: Ein Report darf nie an fehlenden
* Feldern scheitern – alles außer den Metadaten ist `nullable` und darf auch ganz fehlen.
*/
const produkt = z.enum(["pve", "pbs", "pmg"]);
/** Proxmox liefert Datumsangaben als `YYYY-MM-DD`, Zeitstempel teils als Unix-Sekunden. */
const datumWert = z.union([z.string(), z.number()]).nullish();
const nullbarerText = z.string().max(500).nullish();
export const enrollSchema = z.object({
/** Stabile Kennung des Agents, abgeleitet aus /etc/machine-id. */
agentId: z.string().min(8).max(128),
hostname: z.string().min(1).max(255),
fqdn: nullbarerText,
produkt: produkt,
serverId: nullbarerText,
agentVersion: nullbarerText,
});
export const reportSchema = z.object({
/** UUID des Agents; macht nachgesendete Reports idempotent. */
reportId: z.string().min(8).max(64),
agentVersion: nullbarerText,
erhobenAm: z.string().datetime({ offset: true }),
host: z
.object({
hostname: nullbarerText,
fqdn: nullbarerText,
ips: z.array(z.string().max(64)).max(32).nullish(),
clusterName: nullbarerText,
nodeName: nullbarerText,
kernel: nullbarerText,
uptimeSekunden: z.number().int().nonnegative().nullish(),
cpuModell: nullbarerText,
sockets: z.number().int().positive().max(64).nullish(),
paketUpdates: z.number().int().nonnegative().nullish(),
})
.partial()
.default({}),
produkt: z
.object({
typ: produkt.nullish(),
version: nullbarerText,
})
.partial()
.default({}),
subscription: z
.object({
key: nullbarerText,
status: nullbarerText,
level: nullbarerText,
productname: nullbarerText,
sockets: z.number().int().nonnegative().max(64).nullish(),
serverid: nullbarerText,
regdate: datumWert,
nextduedate: datumWert,
checktime: datumWert,
})
.partial()
.default({}),
});
export type EnrollEingabe = z.infer<typeof enrollSchema>;
export type ReportEingabe = z.infer<typeof reportSchema>;
const STATUS_ABBILDUNG: Record<string, SubscriptionStatus> = {
active: "ACTIVE",
new: "ACTIVE",
notfound: "NOTFOUND",
invalid: "INVALID",
expired: "EXPIRED",
suspended: "SUSPENDED",
};
/** Unbekannte Statuswerte werden zu UNKNOWN, damit ein Report nie abgelehnt wird. */
export function statusAusProxmox(wert: string | null | undefined): SubscriptionStatus {
if (!wert) return "UNKNOWN";
return STATUS_ABBILDUNG[wert.trim().toLowerCase()] ?? "UNKNOWN";
}
export function produktAusEingabe(wert: "pve" | "pbs" | "pmg" | null | undefined): Produkt | null {
if (!wert) return null;
return wert === "pve" ? "PVE" : wert === "pbs" ? "PBS" : "PMG";
}
/**
* Wandelt Datumsangaben von Proxmox um: `YYYY-MM-DD`, ISO-Zeitstempel oder Unix-Sekunden.
* Ungültige Werte ergeben `null` statt eines Fehlers.
*/
export function datumAusProxmox(wert: string | number | null | undefined): Date | null {
if (wert === null || wert === undefined || wert === "") return null;
if (typeof wert === "number") {
const ausSekunden = new Date(wert * 1000);
return Number.isNaN(ausSekunden.getTime()) ? null : ausSekunden;
}
const text = wert.trim();
if (/^\d+$/.test(text)) return datumAusProxmox(Number(text));
const datum = /^\d{4}-\d{2}-\d{2}$/.test(text) ? new Date(`${text}T00:00:00Z`) : new Date(text);
return Number.isNaN(datum.getTime()) ? null : datum;
}