diff --git a/src/app/api/admin/skills/pending/[id]/reject/route.ts b/src/app/api/admin/skills/pending/[id]/reject/route.ts index 35216a7..288d2dc 100644 --- a/src/app/api/admin/skills/pending/[id]/reject/route.ts +++ b/src/app/api/admin/skills/pending/[id]/reject/route.ts @@ -8,6 +8,7 @@ import { import { getPackageDef } from "@/lib/packages"; import { listOrgUsers } from "@/lib/zitadel"; import { sendSkillActivationRejectionEmail } from "@/lib/email"; +import { deletePackageSecrets } from "@/lib/openbao"; /** * POST /api/admin/skills/pending/[id]/reject @@ -79,6 +80,33 @@ export async function POST( ); } + // Cleanup: if the package needed customer-provided secrets, the + // user submitted them BEFORE the gate fired (handleSubmitSecrets + // in PackageCard writes to OpenBao then PATCHes). Those secrets + // are now orphaned — the package never made it into spec, won't + // be re-attempted unless the user retries with fresh credentials. + // Best-effort delete: keep the OpenBao path clean, avoid stale + // creds lurking. Idempotent (404 is fine). Failure is logged but + // not propagated — the rejection itself already succeeded. + // + // We deliberately skip customProvisioning packages here. Those + // mint platform-side credentials via a dedicated endpoint and + // need symmetric deprovisioning (POST /[pkg.id] → DELETE + // /[pkg.id]). Calling deletePackageSecrets wouldn't revoke them + // — admin handles that path manually if the rejected request had + // already minted resources. + 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 reject:`, + e + ); + } + } + // Email the requester with the reason — best-effort. try { const orgUsers = await listOrgUsers(req.zitadelOrgId); diff --git a/src/app/api/skills/requests/[id]/withdraw/route.ts b/src/app/api/skills/requests/[id]/withdraw/route.ts index 6c3e828..9f6b966 100644 --- a/src/app/api/skills/requests/[id]/withdraw/route.ts +++ b/src/app/api/skills/requests/[id]/withdraw/route.ts @@ -4,6 +4,8 @@ import { getSkillActivationRequestById, updateSkillActivationRequestStatus, } from "@/lib/db"; +import { getPackageDef } from "@/lib/packages"; +import { deletePackageSecrets } from "@/lib/openbao"; /** * POST /api/skills/requests/[id]/withdraw @@ -50,5 +52,23 @@ export async function POST( { 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); } diff --git a/src/components/packages/package-card.tsx b/src/components/packages/package-card.tsx index f535616..f26e8e1 100644 --- a/src/components/packages/package-card.tsx +++ b/src/components/packages/package-card.tsx @@ -240,11 +240,23 @@ export function PackageCard({ {t("packages.requiresApiKey")} )} {/* Phase 2.5: pending or rejected request takes precedence - over the toggle. Approved/withdrawn never reach here. */} + over the toggle. Approved/withdrawn never reach here. + For packages that needed secrets, surface that they're + safely stored — the user might otherwise worry the + credentials they typed got lost when the activation + was deferred. */} {canEdit && activationRequest?.status === "pending" ? ( -