import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { agiloftRetrieveContract } from '@/lib/api/contracts/tools/agiloft' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils' import { agiloftLoginPinned, agiloftLogoutPinned, resolveAgiloftInstance, } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' const logger = createLogger('AgiloftRetrieveAPI') export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized Agiloft retrieve attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, { status: 401 } ) } const parsed = await parseRequest( agiloftRetrieveContract, request, {}, { validationErrorResponse: (error) => { logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues }) return NextResponse.json( { success: false, error: getValidationErrorMessage(error, 'Invalid request data'), details: error.issues, }, { status: 400 } ) }, } ) if (!parsed.success) return parsed.response const data = parsed.data.body let resolvedIP: string try { resolvedIP = await resolveAgiloftInstance(data.instanceUrl) } catch (error) { logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, { instanceUrl: data.instanceUrl, }) return NextResponse.json({ success: false, error: toError(error).message }, { status: 400 }) } const token = await agiloftLoginPinned(data, resolvedIP) const base = data.instanceUrl.replace(/\/$/, '') try { const url = buildRetrieveAttachmentUrl(base, data) logger.info(`[${requestId}] Downloading attachment from Agiloft`, { recordId: data.recordId, fieldName: data.fieldName, position: data.position, }) const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'GET', headers: { Authorization: `Bearer ${token}`, }, }) if (!agiloftResponse.ok) { const errorText = await agiloftResponse.text() logger.error( `[${requestId}] Agiloft retrieve error: ${agiloftResponse.status} - ${errorText}` ) return NextResponse.json( { success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` }, { status: agiloftResponse.status } ) } const contentType = agiloftResponse.headers.get('content-type') || 'application/octet-stream' const contentDisposition = agiloftResponse.headers.get('content-disposition') let fileName = 'attachment' if (contentDisposition) { const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) if (match?.[1]) { fileName = match[1].replace(/['"]/g, '') } } const arrayBuffer = await agiloftResponse.arrayBuffer() const fileBuffer = Buffer.from(arrayBuffer) logger.info(`[${requestId}] Attachment downloaded successfully`, { name: fileName, size: fileBuffer.length, mimeType: contentType, }) const base64Data = fileBuffer.toString('base64') return NextResponse.json({ success: true, output: { file: { name: fileName, mimeType: contentType, data: base64Data, size: fileBuffer.length, }, }, }) } finally { await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP) } } catch (error) { logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) } })