import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { agiloftAttachContract } 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 type { RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' import { buildAttachFileUrl } from '@/tools/agiloft/utils' import { agiloftLoginPinned, agiloftLogoutPinned, resolveAgiloftInstance, } from '@/tools/agiloft/utils.server' export const dynamic = 'force-dynamic' const logger = createLogger('AgiloftAttachAPI') export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, { status: 401 } ) } const parsed = await parseRequest( agiloftAttachContract, 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 if (!data.file) { return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 }) } const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger) if (userFiles.length === 0) { return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 }) } const userFile = userFiles[0] logger.info( `[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)` ) const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) if (denied) return denied let fileBuffer: Buffer try { const servable = await downloadServableFileFromStorage(userFile, requestId, logger) fileBuffer = servable.buffer } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady logger.error(`[${requestId}] Failed to download file from storage:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) } const resolvedFileName = data.fileName || userFile.name || 'attachment' 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 = buildAttachFileUrl(base, data, resolvedFileName) logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`) const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream', Authorization: `Bearer ${token}`, }, body: new Uint8Array(fileBuffer), }) if (!agiloftResponse.ok) { const errorText = await agiloftResponse.text() logger.error( `[${requestId}] Agiloft attach error: ${agiloftResponse.status} - ${errorText}` ) return NextResponse.json( { success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` }, { status: agiloftResponse.status } ) } let totalAttachments = 0 const responseText = await agiloftResponse.text() try { const responseData = JSON.parse(responseText) const result = responseData.result ?? responseData totalAttachments = typeof result === 'number' ? result : (result.count ?? result.total ?? 1) } catch { totalAttachments = Number(responseText) || 1 } logger.info( `[${requestId}] File attached successfully. Total attachments: ${totalAttachments}` ) return NextResponse.json({ success: true, output: { recordId: data.recordId.trim(), fieldName: data.fieldName.trim(), fileName: resolvedFileName, totalAttachments, }, }) } finally { await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP) } } catch (error) { logger.error(`[${requestId}] Error attaching file to Agiloft:`, error) return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) } })