import { NextResponse } from "next/server"; import { getSessionUser } from "@/lib/session"; import { getSkillActivationRequestById, updateSkillActivationRequestStatus, } from "@/lib/db"; import { getPackageDef } from "@/lib/packages"; import { deletePackageSecrets } from "@/lib/openbao"; /** * POST /api/skills/requests/[id]/withdraw * * The owner of a pending activation request can cancel it. This * doesn't touch K8s (the skill was never enabled) — it just flips * the row to 'withdrawn' so the user's UI clears the pending * state and they can try a different skill or retry later. * * Authorization: only the original requester OR a platform admin * can withdraw a request. We deliberately don't allow other org * members to cancel each other's requests in v1 — the partial * unique index would let one user repeatedly cancel another's * pending request. */ export async function POST( _request: Request, { params }: { params: Promise<{ id: string }> } ) { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { id } = await params; const req = await getSkillActivationRequestById(id); if (!req) { return NextResponse.json({ error: "Request not found" }, { status: 404 }); } if (!user.isPlatform && req.zitadelUserId !== user.id) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } if (req.status !== "pending") { return NextResponse.json( { error: `Request is already ${req.status}` }, { status: 409 } ); } const updated = await updateSkillActivationRequestStatus(id, "withdrawn", { reviewedBy: user.id, }); if (!updated) { return NextResponse.json( { error: "Request status changed during withdraw." }, { status: 409 } ); } // Cleanup: same logic as reject — the user submitted secrets // before the gate fired, and those are now orphaned in OpenBao. // Best-effort delete; failure logged but not propagated. Skip // customProvisioning packages (their deprovisioning is a // separate, dedicated endpoint). const def = getPackageDef(req.skillId); if (def?.requiresSecrets && !def.customProvisioning) { try { await deletePackageSecrets(req.tenantName, req.skillId); } catch (e) { console.error( `Failed to delete orphan secrets for ${req.tenantName}/${req.skillId} after withdraw:`, e ); } } return NextResponse.json(updated); }