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>
This commit is contained in:
2026-09-17 20:39:33 +02:00
co-authored by Claude Opus 5
parent 51c8aafa69
commit bef51eb863
19 changed files with 1209 additions and 30 deletions
+45
View File
@@ -0,0 +1,45 @@
import {
AGENT_VERSION,
bearerToken,
enrollRateLimit,
fehlerAntwort,
fehlerBehandeln,
jsonEingabe,
rateLimitPruefen,
} from "@/server/agent/http";
import { enrollSchema } from "@/server/agent/schema";
import { registriereAgent } from "@/server/agent/service";
export const dynamic = "force-dynamic";
/** Registrierung eines Agents mit einem Enrollment-Token. */
export async function POST(request: Request) {
const token = bearerToken(request);
const gebremst = rateLimitPruefen(enrollRateLimit, request.headers, token);
if (gebremst) return gebremst;
if (!token) {
return fehlerAntwort(401, "token_fehlt", "Enrollment-Token fehlt (Authorization: Bearer …).");
}
const eingabe = await jsonEingabe(request, enrollSchema);
if ("antwort" in eingabe) return eingabe.antwort;
try {
const ergebnis = await registriereAgent(eingabe.daten, token);
return Response.json(
{
ok: true,
hostId: ergebnis.hostId,
token: ergebnis.token,
bestehenderHost: ergebnis.bestehenderHost,
serverIdBereitsBekannt: ergebnis.serverIdBereitsBekannt,
agentVersion: AGENT_VERSION,
serverZeit: new Date().toISOString(),
},
{ status: ergebnis.bestehenderHost ? 200 : 201 },
);
} catch (fehler) {
return fehlerBehandeln(fehler);
}
}
+35
View File
@@ -0,0 +1,35 @@
import {
AGENT_VERSION,
agentRateLimit,
bearerToken,
fehlerAntwort,
fehlerBehandeln,
rateLimitPruefen,
} from "@/server/agent/http";
import { authentifiziereHost } from "@/server/agent/service";
export const dynamic = "force-dynamic";
/** Verbindungstest für `proxledger-agent --test`. */
export async function GET(request: Request) {
const token = bearerToken(request);
const gebremst = rateLimitPruefen(agentRateLimit, request.headers, token);
if (gebremst) return gebremst;
if (!token) {
return fehlerAntwort(401, "token_fehlt", "Host-Token fehlt (Authorization: Bearer …).");
}
try {
const host = await authentifiziereHost(token);
return Response.json({
ok: true,
hostId: host.id,
hostname: host.hostname,
agentVersion: AGENT_VERSION,
serverZeit: new Date().toISOString(),
});
} catch (fehler) {
return fehlerBehandeln(fehler);
}
}
+44
View File
@@ -0,0 +1,44 @@
import { clientIp } from "@/lib/ratelimit";
import {
AGENT_VERSION,
agentRateLimit,
bearerToken,
fehlerAntwort,
fehlerBehandeln,
jsonEingabe,
rateLimitPruefen,
} from "@/server/agent/http";
import { reportSchema } from "@/server/agent/schema";
import { authentifiziereHost, verarbeiteReport } from "@/server/agent/service";
export const dynamic = "force-dynamic";
/** Täglicher Statusbericht eines Hosts. */
export async function POST(request: Request) {
const token = bearerToken(request);
const gebremst = rateLimitPruefen(agentRateLimit, request.headers, token);
if (gebremst) return gebremst;
if (!token) {
return fehlerAntwort(401, "token_fehlt", "Host-Token fehlt (Authorization: Bearer …).");
}
const eingabe = await jsonEingabe(request, reportSchema);
if ("antwort" in eingabe) return eingabe.antwort;
try {
const host = await authentifiziereHost(token);
const ergebnis = await verarbeiteReport(host, eingabe.daten, clientIp(request.headers));
return Response.json({
ok: true,
hostId: host.id,
doppelt: ergebnis.doppelt,
laufzeitVorschlag: ergebnis.laufzeitVorschlagAngelegt,
agentVersion: AGENT_VERSION,
serverZeit: new Date().toISOString(),
});
} catch (fehler) {
return fehlerBehandeln(fehler);
}
}