import { NextResponse } from 'next/server'; import { withAuth, withPermission } from '@/lib/api/helpers'; import { createAuditLog } from '@/lib/audit'; import { errorResponse, NotFoundError } from '@/lib/errors'; import { registryFor } from '@/lib/settings/registry'; import { getSetting } from '@/lib/settings/resolver'; /** * POST /api/v1/admin/settings/:key/reveal * * Returns the decrypted cleartext for an encrypted / sensitive setting. * Used by the eye-toggle on encrypted fields in the registry-driven admin * form so the operator can verify what they saved earlier. * * Gated on `admin.manage_settings` (the same permission required to write * the value — so this never widens an existing trust boundary). Every * reveal is audit-logged with the request id so a super-admin can trace * who looked at what and when. * * Refuses to reveal values resolved from `env` or `default` — those would * leak server-process secrets via the API. */ export const POST = withAuth( withPermission('admin', 'manage_settings', async (_req, ctx, params) => { try { const key = params.key!; const entry = registryFor(key); if (!entry) throw new NotFoundError(`Unknown setting: ${key}`); if (!entry.encrypted && !entry.sensitive) { // Non-sensitive values are already returned in the resolved-list // endpoint, so a dedicated reveal isn't needed (and could be // misused to bypass observability). return NextResponse.json({ data: { revealed: false, value: null } }, { status: 200 }); } // Resolve through the standard chain so the user sees exactly what // the runtime would. The resolver decrypts on the way out. const value = await getSetting(key, ctx.portId); void createAuditLog({ userId: ctx.userId, portId: ctx.portId, action: 'view', entityType: 'setting', entityId: key, metadata: { settingKey: key, op: 'reveal' }, ipAddress: ctx.ipAddress, userAgent: ctx.userAgent, }); return NextResponse.json({ data: { revealed: true, value: value ?? null } }); } catch (error) { return errorResponse(error); } }), );