import { randomUUID } from "node:crypto"; import { isRequestTimestampFresh, verifyHashedToken } from "@minecraft-account-manager/auth"; import { problemDetails, velocityConnectionRequestSchema } from "@minecraft-account-manager/contracts"; import { events, minecraftAccounts, pluginCredentials, pluginRequests } from "@minecraft-account-manager/database"; import { and, eq, isNull, lt } from "drizzle-orm"; import { NextResponse } from "next/server"; import { db } from "@/lib/database"; import { isUniqueConstraintViolation } from "@/lib/database-errors"; import { logger } from "@/lib/logger"; import { problemInstance, problemResponse } from "@/lib/problem-response"; const MAX_CLOCK_SKEW_MS = 45_000; function methodNotAllowed(request: Request) { const response = problemResponse(problemDetails( "urn:error:method-not-allowed", "Method not allowed", 405, "This endpoint only accepts POST requests.", problemInstance(request), )); response.headers.set("allow", "POST"); return response; } export const GET = methodNotAllowed; export const PUT = methodNotAllowed; export const PATCH = methodNotAllowed; export const DELETE = methodNotAllowed; export async function POST(request: Request) { const instance = problemInstance(request); const authorization = request.headers.get("authorization") ?? ""; const token = authorization.startsWith("Bearer ") ? authorization.slice(7).trim() : ""; if (!token) return problemResponse(problemDetails( "urn:error:unauthorized", "Unauthorized", 401, "A valid Velocity server credential is required.", instance, )); const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase(); if (mediaType !== "application/json") return problemResponse(problemDetails( "urn:error:unsupported-media-type", "Unsupported media type", 415, "Velocity connection reports must use application/json.", instance, )); const parsed = velocityConnectionRequestSchema.safeParse(await request.json().catch(() => null)); if (!parsed.success) return problemResponse(problemDetails( "urn:error:invalid-velocity-connection-request", "Invalid Velocity connection report", 400, "The request body does not match the required Velocity connection contract.", instance, { issues: parsed.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code })) }, )); const input = parsed.data; const occurredAt = new Date(input.occurredAt); if (!isRequestTimestampFresh(occurredAt, new Date(), MAX_CLOCK_SKEW_MS)) return problemResponse(problemDetails( "urn:error:expired-velocity-connection-request", "Expired Velocity connection report", 401, "The request timestamp is outside the allowed clock-skew window.", instance, )); const [credential] = await db .select({ secretHash: pluginCredentials.secretHash }) .from(pluginCredentials) .where(and(eq(pluginCredentials.serverId, input.serverId), isNull(pluginCredentials.revokedAt))) .limit(1); if (!credential || !verifyHashedToken(token, credential.secretHash)) return problemResponse(problemDetails( "urn:error:unauthorized", "Unauthorized", 401, "The Velocity server credential is invalid or revoked.", instance, )); try { const recorded = await db.transaction(async (tx) => { await tx.delete(pluginRequests).where(lt(pluginRequests.expiresAt, new Date())); await tx.insert(pluginRequests).values({ requestId: input.requestId, serverId: input.serverId, receivedAt: new Date(), expiresAt: new Date(Date.now() + 5 * 60_000), }); const [account] = await tx .select({ id: minecraftAccounts.id, userId: minecraftAccounts.userId }) .from(minecraftAccounts) .where(and(eq(minecraftAccounts.minecraftUuid, input.minecraftUuid), isNull(minecraftAccounts.deletedAt))) .limit(1); if (!account) return false; await tx.insert(events).values({ id: randomUUID(), source: `/velocity/${input.serverId}`, type: "games.minecraft.account-manager.game.player.connected", subject: `minecraft-account/${account.id}`, time: occurredAt, actorUserId: account.userId, correlationId: input.requestId, data: { username: input.username, minecraftUuid: input.minecraftUuid, serverId: input.serverId, }, }); return true; }); if (!recorded) return problemResponse(problemDetails( "urn:error:unknown-minecraft-account", "Unknown Minecraft account", 404, "The connected Minecraft account is no longer registered.", instance, )); return new NextResponse(null, { status: 204 }); } catch (error) { if (isUniqueConstraintViolation(error, "plugin_requests_pkey")) return problemResponse(problemDetails( "urn:error:replayed-velocity-connection-request", "Velocity request replayed", 409, "This Velocity request ID has already been processed.", instance, )); logger.error({ err: error, event: "velocity.connection_report_failed" }, "Failed to record a confirmed Velocity connection"); return problemResponse(problemDetails( "urn:error:service-unavailable", "Service unavailable", 503, "The connection report could not be recorded.", instance, )); } }