import { NextRequest, NextResponse } from 'next/server'; import { requireAdminAuth, getClientIP } from '@/lib/admin/session'; import { auditLog } from '@/lib/admin/audit'; import { logger } from '@/lib/logger'; import { listApprovals, decideApproval, revokeApproval } from '@/lib/admin/plugin-approvals'; /** * Admin-protected CRUD for the per-(pluginId, bundleHash) approval table. * * GET /api/admin/plugin-approvals → list all entries * POST /api/admin/plugin-approvals → { pluginId, bundleHash, decision: 'approved'|'denied' } * DELETE /api/admin/plugin-approvals?pluginId=…&bundleHash=… → revoke */ function isValidId(s: unknown): s is string { return typeof s === 'string' && /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(s) && s.length <= 64; } function isValidHash(s: unknown): s is string { return typeof s === 'string' && /^[a-f0-9]{16,128}$/i.test(s); } export async function GET(request: NextRequest) { try { const result = await requireAdminAuth(request); if ('error' in result) return result.error; const entries = await listApprovals(); return NextResponse.json({ entries }, { headers: { 'Cache-Control': 'no-store' } }); } catch (err) { logger.error('plugin-approvals GET', { error: err instanceof Error ? err.message : String(err) }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } export async function POST(request: NextRequest) { try { const result = await requireAdminAuth(request); if ('error' in result) return result.error; // AdminSessionPayload carries only role/iat/exp; we use a stable label // for the audit trail rather than a per-user identity. const adminUser = 'admin'; void result; const ip = getClientIP(request); let body: unknown; try { body = await request.json(); } catch { body = null; } const b = (body ?? {}) as { pluginId?: unknown; bundleHash?: unknown; decision?: unknown }; if (!isValidId(b.pluginId) || !isValidHash(b.bundleHash)) { return NextResponse.json({ error: 'invalid pluginId or bundleHash' }, { status: 400 }); } if (b.decision !== 'approved' && b.decision !== 'denied') { return NextResponse.json({ error: 'decision must be "approved" or "denied"' }, { status: 400 }); } const entry = await decideApproval(b.pluginId, b.bundleHash, b.decision, adminUser); await auditLog('plugin.approval', { pluginId: entry.pluginId, bundleHash: entry.bundleHash, decision: entry.status }, ip); return NextResponse.json({ entry }); } catch (err) { logger.error('plugin-approvals POST', { error: err instanceof Error ? err.message : String(err) }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } export async function DELETE(request: NextRequest) { try { const result = await requireAdminAuth(request); if ('error' in result) return result.error; // AdminSessionPayload carries only role/iat/exp; we use a stable label // for the audit trail rather than a per-user identity. const adminUser = 'admin'; void result; const ip = getClientIP(request); const pluginId = request.nextUrl.searchParams.get('pluginId'); const bundleHash = request.nextUrl.searchParams.get('bundleHash'); if (!isValidId(pluginId) || !isValidHash(bundleHash)) { return NextResponse.json({ error: 'invalid pluginId or bundleHash' }, { status: 400 }); } await revokeApproval(pluginId, bundleHash); await auditLog('plugin.approval.revoke', { pluginId, bundleHash, by: adminUser }, ip); return NextResponse.json({ ok: true }); } catch (err) { logger.error('plugin-approvals DELETE', { error: err instanceof Error ? err.message : String(err) }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }