import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { type NextRequest, NextResponse } from 'next/server' import { v1ListKnowledgeDocumentsContract, v1UploadKnowledgeDocumentContract, } from '@/lib/api/contracts/v1/knowledge' import { parseRequest } from '@/lib/api/server' import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor' import { isPayloadSizeLimitError, MAX_MULTIPART_OVERHEAD_BYTES, readFormDataWithLimit, } from '@/lib/core/utils/stream-limits' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createSingleDocument, type DocumentData, getDocuments, processDocumentsWithQueue, } from '@/lib/knowledge/documents/service' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace' import { validateFileType } from '@/lib/uploads/utils/validation' import { handleError, resolveKnowledgeBase, serializeDate } from '@/app/api/v1/knowledge/utils' import { authenticateRequest } from '@/app/api/v1/middleware' export const dynamic = 'force-dynamic' export const revalidate = 0 const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB interface DocumentsRouteParams { params: Promise<{ id: string }> } /** GET /api/v1/knowledge/[id]/documents — List documents in a knowledge base. */ export const GET = withRouteHandler(async (request: NextRequest, context: DocumentsRouteParams) => { const auth = await authenticateRequest(request, 'knowledge-detail') if (auth instanceof NextResponse) return auth const { requestId, userId, rateLimit } = auth try { const parsed = await parseRequest(v1ListKnowledgeDocumentsContract, request, context) if (!parsed.success) return parsed.response const { workspaceId, limit, offset, search, enabledFilter, sortBy, sortOrder } = parsed.data.query const { id: knowledgeBaseId } = parsed.data.params const result = await resolveKnowledgeBase(knowledgeBaseId, workspaceId, userId, rateLimit) if (result instanceof NextResponse) return result const documentsResult = await getDocuments( knowledgeBaseId, { enabledFilter: enabledFilter === 'all' ? undefined : enabledFilter, search, limit, offset, sortBy: sortBy as DocumentSortField, sortOrder: sortOrder as SortOrder, }, requestId ) return NextResponse.json({ success: true, data: { documents: documentsResult.documents.map((doc) => ({ id: doc.id, knowledgeBaseId, filename: doc.filename, fileSize: doc.fileSize, mimeType: doc.mimeType, processingStatus: doc.processingStatus, chunkCount: doc.chunkCount, tokenCount: doc.tokenCount, characterCount: doc.characterCount, enabled: doc.enabled, createdAt: serializeDate(doc.uploadedAt), })), pagination: documentsResult.pagination, }, }) } catch (error) { return handleError(requestId, error, 'Failed to list documents') } }) /** POST /api/v1/knowledge/[id]/documents — Upload a document to a knowledge base. */ export const POST = withRouteHandler( async (request: NextRequest, context: DocumentsRouteParams) => { const auth = await authenticateRequest(request, 'knowledge-detail') if (auth instanceof NextResponse) return auth const { requestId, userId, rateLimit } = auth try { const parsed = await parseRequest(v1UploadKnowledgeDocumentContract, request, context) if (!parsed.success) return parsed.response const { id: knowledgeBaseId } = parsed.data.params let formData: FormData try { formData = await readFormDataWithLimit(request, { maxBytes: MAX_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES, label: 'knowledge document upload body', }) } catch (error) { if (isPayloadSizeLimitError(error)) { return NextResponse.json({ error: error.message }, { status: 413 }) } return NextResponse.json( { error: 'Request body must be valid multipart form data' }, { status: 400 } ) } const rawFile = formData.get('file') const file = rawFile instanceof File ? rawFile : null const rawWorkspaceId = formData.get('workspaceId') const workspaceId = typeof rawWorkspaceId === 'string' ? rawWorkspaceId : null if (!workspaceId) { return NextResponse.json({ error: 'workspaceId form field is required' }, { status: 400 }) } if (!file) { return NextResponse.json({ error: 'file form field is required' }, { status: 400 }) } if (file.size > MAX_FILE_SIZE) { return NextResponse.json( { error: `File size exceeds 100MB limit (${(file.size / (1024 * 1024)).toFixed(2)}MB)`, }, { status: 413 } ) } const fileTypeError = validateFileType(file.name, file.type || '') if (fileTypeError) { return NextResponse.json({ error: fileTypeError.message }, { status: 415 }) } const result = await resolveKnowledgeBase( knowledgeBaseId, workspaceId, userId, rateLimit, 'write' ) if (result instanceof NextResponse) return result // Fast usage gate before the storage write + indexing (the async backstop // in processDocumentAsync still covers non-HTTP paths). const usage = await checkActorUsageLimits(userId, workspaceId) if (usage.isExceeded) { return NextResponse.json( { error: usage.message || 'Usage limit exceeded. Please upgrade your plan to continue.', }, { status: 402 } ) } const buffer = Buffer.from(await file.arrayBuffer()) const contentType = file.type || 'application/octet-stream' const uploadedFile = await uploadWorkspaceFile( workspaceId, userId, buffer, file.name, contentType ) const newDocument = await createSingleDocument( { filename: file.name, fileUrl: uploadedFile.url, fileSize: file.size, mimeType: contentType, }, knowledgeBaseId, requestId, userId ) const documentData: DocumentData = { documentId: newDocument.id, filename: file.name, fileUrl: uploadedFile.url, fileSize: file.size, mimeType: contentType, } processDocumentsWithQueue([documentData], knowledgeBaseId, {}, requestId).catch(() => { // Processing errors are logged internally }) recordAudit({ workspaceId, actorId: userId, action: AuditAction.DOCUMENT_UPLOADED, resourceType: AuditResourceType.DOCUMENT, resourceId: newDocument.id, resourceName: file.name, description: `Uploaded document "${file.name}" to knowledge base via API`, metadata: { knowledgeBaseId, fileSize: file.size, mimeType: contentType }, request, }) return NextResponse.json({ success: true, data: { document: { id: newDocument.id, knowledgeBaseId, filename: newDocument.filename, fileSize: newDocument.fileSize, mimeType: newDocument.mimeType, processingStatus: 'pending', chunkCount: 0, tokenCount: 0, characterCount: 0, enabled: newDocument.enabled, createdAt: serializeDate(newDocument.uploadedAt), }, message: 'Document uploaded successfully. Processing will begin shortly.', }, }) } catch (error) { return handleError(requestId, error, 'Failed to upload document') } } )