import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { listCustomBlocksContract, publishCustomBlockContract, } from '@/lib/api/contracts/custom-blocks' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { CustomBlockValidationError, type CustomBlockWithInputs, listCustomBlocksWithInputs, publishCustomBlock, } from '@/lib/workflows/custom-blocks/operations' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CustomBlocksAPI') /** Wire shape for a custom block. Keeps the icon field name explicit for the client. */ function toWire(block: CustomBlockWithInputs) { return { id: block.id, organizationId: block.organizationId, workflowId: block.workflowId, workflowName: block.workflowName, workspaceId: block.workspaceId, workspaceName: block.workspaceName, type: block.type, name: block.name, description: block.description, iconUrl: block.iconUrl, enabled: block.enabled, inputFields: block.inputFields, exposedOutputs: block.exposedOutputs, } } export const GET = withRouteHandler(async (request: NextRequest) => { const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(listCustomBlocksContract, request, {}) if (!parsed.success) return parsed.response const userId = session.user.id const { workspaceId } = parsed.data.query const access = await checkWorkspaceAccess(workspaceId, userId) if (!access.hasAccess) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } const organizationId = access.workspace?.organizationId if (!organizationId) { return NextResponse.json({ enabled: false, customBlocks: [] }) } if (!(await isFeatureEnabled('deploy-as-block', { userId, orgId: organizationId }))) { return NextResponse.json({ enabled: false, customBlocks: [] }) } const enabled = await isOrganizationOnEnterprisePlan(organizationId) const blocks = enabled ? await listCustomBlocksWithInputs(organizationId) : [] return NextResponse.json({ enabled, customBlocks: blocks.map(toWire) }) }) export const POST = withRouteHandler(async (request: NextRequest) => { const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(publishCustomBlockContract, request, {}) if (!parsed.success) return parsed.response const userId = session.user.id const { workspaceId, workflowId, name, description, iconUrl, inputs, exposedOutputs } = parsed.data.body const access = await checkWorkspaceAccess(workspaceId, userId) if (!access.canAdmin) { return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 }) } const organizationId = access.workspace?.organizationId if (!organizationId) { return NextResponse.json( { error: 'Publishing a block requires the workspace to belong to an organization' }, { status: 400 } ) } if (!(await isFeatureEnabled('deploy-as-block', { userId, orgId: organizationId }))) { return NextResponse.json({ error: 'Deploy as block is not enabled' }, { status: 403 }) } if (!(await isOrganizationOnEnterprisePlan(organizationId))) { return NextResponse.json( { error: 'Deploy as block requires an enterprise plan' }, { status: 403 } ) } try { const block = await publishCustomBlock({ organizationId, workspaceId, workflowId, userId, name, description, iconUrl, inputs, exposedOutputs, }) recordAudit({ workspaceId, actorId: userId, actorName: session.user.name, actorEmail: session.user.email, action: AuditAction.CUSTOM_BLOCK_PUBLISHED, resourceType: AuditResourceType.CUSTOM_BLOCK, resourceId: block.id, resourceName: block.name, description: `Published custom block "${block.name}"`, metadata: { organizationId, type: block.type, workflowId }, request, }) return NextResponse.json({ customBlock: toWire(block) }) } catch (error) { if (error instanceof CustomBlockValidationError) { return NextResponse.json({ error: error.message }, { status: 400 }) } logger.error('Failed to publish custom block', { error: getErrorMessage(error) }) throw error } })