import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { getSessionUser } from "@/lib/session"; import { getTenant, patchTenantSpec } from "@/lib/k8s"; import { safeError } from "@/lib/errors"; /** * Per-tenant OpenClaw image override (admin-only). * * Why admin-only: customers cannot pick OpenClaw versions. This * exists so the platform team can A/B-test new releases on specific * tenants without rolling them out fleet-wide. The endpoint enforces * `user.isPlatform`; even owners of the tenant's org cannot use it. * * PATCH body shapes: * - { tag: "2026.4.22" } → use this tag * - { tag: "" } or empty body → clear override (revert to platform * default) * * Tag-only by design — see operator notes for rationale. */ const patchSchema = z.object({ tag: z.string().trim().max(256).optional(), }); export async function PATCH( req: NextRequest, { params }: { params: Promise<{ name: string }> } ) { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!user.isPlatform) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const { name } = await params; const tenant = await getTenant(name); if (!tenant) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } const body = await req.json().catch(() => null); const parsed = patchSchema.safeParse(body ?? {}); if (!parsed.success) { return NextResponse.json( { error: "Invalid input", details: parsed.error.flatten() }, { status: 400 } ); } const tag = parsed.data.tag ?? ""; const isClearing = tag === ""; // Merge-patch semantics: openClawImage: null removes the field // from the spec; openClawImage: { tag } sets it. const spec: any = isClearing ? { openClawImage: null } : { openClawImage: { tag } }; try { const updated = await patchTenantSpec(name, spec); return NextResponse.json({ message: isClearing ? "Override cleared; tenant follows platform default." : "Override set.", openClawImage: updated.spec.openClawImage ?? null, }); } catch (e: any) { console.error("Failed to set tenant openclaw image:", e); return NextResponse.json( { error: safeError(e, "Failed to update tenant image") }, { status: 500 } ); } }